diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 0000000..4247355 --- /dev/null +++ b/.bazelignore @@ -0,0 +1,9 @@ +.direnv +.pnpm-store +bazel-bin +bazel-out +bazel-testlogs +coverage +dist +dist-types +node_modules diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 625a799..f93ccd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,8 +25,8 @@ jobs: typecheck_command: pnpm run check unit_test_command: pnpm run test build_command: pnpm run build - package_check_command: pnpm run check:package - bazel_targets: "//:pkg //:typecheck //:test" + package_check_command: "pnpm run check:package && pnpm run check:bundle-size" + bazel_targets: "//:pkg //:package_consumer_check //:bundle_size_check //:typecheck //:test" package_dir: ./bazel-bin/pkg npm_access: public dry_run: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bf7aafc..7b859ca 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,8 +25,8 @@ jobs: typecheck_command: pnpm run check unit_test_command: pnpm run test build_command: pnpm run build - package_check_command: pnpm run check:package - bazel_targets: "//:pkg //:typecheck //:test" + package_check_command: "pnpm run check:package && pnpm run check:bundle-size" + bazel_targets: "//:pkg //:package_consumer_check //:bundle_size_check //:typecheck //:test" package_dir: ./bazel-bin/pkg npm_access: public dry_run: false diff --git a/BUILD.bazel b/BUILD.bazel index 1d9fc84..9a90022 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -14,11 +14,10 @@ Build strategy: - vitest for PBT tests (property-based tests using fast-check) """ -load("@aspect_rules_js//js:defs.bzl", "js_library", "js_run_binary") +load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_library", "js_run_binary") load("@aspect_rules_js//npm:defs.bzl", "npm_package") load("@tummycrypt_tinyvectors_npm//:defs.bzl", "npm_link_all_packages") load("@tummycrypt_tinyvectors_npm//:svelte-check/package_json.bzl", svelte_check_bin = "bin") -load("@tummycrypt_tinyvectors_npm//:typescript/package_json.bzl", typescript_bin = "bin") load("@tummycrypt_tinyvectors_npm//:vite/package_json.bzl", vite_bin = "bin") load("@tummycrypt_tinyvectors_npm//:vitest/package_json.bzl", vitest_bin = "bin") @@ -29,9 +28,37 @@ load("@tummycrypt_tinyvectors_npm//:vitest/package_json.bzl", vitest_bin = "bin" npm_link_all_packages(name = "node_modules") vite_bin.vite_binary(name = "vite") -typescript_bin.tsc_binary(name = "tsc") svelte_check_bin.svelte_check_binary(name = "svelte_check") +js_binary( + name = "build_declarations", + data = [ + ":node_modules", + ":node_modules/typescript", + "scripts/copy-svelte-declarations.mjs", + ], + entry_point = "scripts/build-declarations.mjs", +) + +js_binary( + name = "check_package_consumer", + data = [ + ":node_modules", + ":node_modules/svelte", + ":node_modules/typescript", + ], + entry_point = "scripts/check-package-consumer.mjs", +) + +js_binary( + name = "check_bundle_size", + data = [ + ":node_modules", + ":node_modules/vite", + ], + entry_point = "scripts/check-bundle-size.mjs", +) + # ============================================================================= # JavaScript package build @@ -73,19 +100,18 @@ js_run_binary( js_run_binary( name = "tinyvectors_declarations", - tool = "//:tsc", + tool = "//:build_declarations", srcs = glob([ "src/**/*.ts", + "src/**/*.svelte.d.ts", ]) + [ "package.json", "pnpm-lock.yaml", + "scripts/copy-svelte-declarations.mjs", "tsconfig.json", "tsconfig.declarations.json", ":node_modules", - ], - args = [ - "-p", - "$(execpath tsconfig.declarations.json)", + ":node_modules/typescript", ], out_dirs = ["dist-types"], visibility = ["//visibility:public"], @@ -121,6 +147,37 @@ npm_package( visibility = ["//visibility:public"], ) +js_run_binary( + name = "package_consumer_check", + tool = "//:check_package_consumer", + srcs = [ + ":pkg", + ":node_modules", + ":node_modules/svelte", + ":node_modules/typescript", + ], + args = [ + "$(rootpath :pkg)", + ], + stdout = "package-consumer-check.log", + visibility = ["//visibility:public"], +) + +js_run_binary( + name = "bundle_size_check", + tool = "//:check_bundle_size", + srcs = [ + ":pkg", + ":node_modules", + ":node_modules/vite", + ], + args = [ + "$(rootpath :pkg)", + ], + stdout = "bundle-size-check.log", + visibility = ["//visibility:public"], +) + # ============================================================================= # Test targets # diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index ef2a4eb..bd64d2a 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -174,6 +174,160 @@ }, "selectedYankedVersions": {}, "moduleExtensions": { + "@@aspect_rules_js+//npm:extensions.bzl%pnpm": { + "general": { + "bzlTransitiveDigest": "+38sdUshoDaaBOpNBG17zT25s0y5uiGlxqyEtzgmzVs=", + "usagesDigest": "NY1vao0VHjdP6l6u+OOM5scvRTwDos1a0xhs0ZBM2Go=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "pnpm": { + "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_rule", + "attributes": { + "package": "pnpm", + "version": "9.15.9", + "root_package": "", + "link_workspace": "", + "link_packages": {}, + "integrity": "sha512-aARhQYk8ZvrQHAeSMRKOmvuJ74fiaR1p5NQO7iKJiClf1GghgbrlW1hBjDolO95lpQXsfF+UA+zlzDzTfc8lMQ==", + "url": "", + "commit": "", + "patch_args": [ + "-p0" + ], + "patches": [], + "custom_postinstall": "", + "npm_auth": "", + "npm_auth_basic": "", + "npm_auth_username": "", + "npm_auth_password": "", + "lifecycle_hooks": [], + "extra_build_content": "load(\"@aspect_rules_js//js:defs.bzl\", \"js_binary\")\njs_binary(name = \"pnpm\", data = glob([\"package/**\"]), entry_point = \"package/dist/pnpm.cjs\", visibility = [\"//visibility:public\"])", + "generate_bzl_library_targets": false, + "extract_full_archive": true, + "exclude_package_contents": [] + } + }, + "pnpm__links": { + "repoRuleId": "@@aspect_rules_js+//npm/private:npm_import.bzl%npm_import_links", + "attributes": { + "package": "pnpm", + "version": "9.15.9", + "dev": false, + "root_package": "", + "link_packages": {}, + "deps": {}, + "transitive_closure": {}, + "lifecycle_build_target": false, + "lifecycle_hooks_env": [], + "lifecycle_hooks_execution_requirements": [ + "no-sandbox" + ], + "lifecycle_hooks_use_default_shell_env": false, + "bins": {}, + "package_visibility": [ + "//visibility:public" + ], + "replace_package": "", + "exclude_package_contents": [] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "aspect_bazel_lib+", + "bazel_lib", + "bazel_lib+" + ], + [ + "aspect_bazel_lib+", + "bazel_skylib", + "bazel_skylib+" + ], + [ + "aspect_bazel_lib+", + "bazel_tools", + "bazel_tools" + ], + [ + "aspect_bazel_lib+", + "tar.bzl", + "tar.bzl+" + ], + [ + "aspect_rules_js+", + "aspect_bazel_lib", + "aspect_bazel_lib+" + ], + [ + "aspect_rules_js+", + "aspect_rules_js", + "aspect_rules_js+" + ], + [ + "aspect_rules_js+", + "aspect_tools_telemetry_report", + "aspect_tools_telemetry++telemetry+aspect_tools_telemetry_report" + ], + [ + "aspect_rules_js+", + "bazel_features", + "bazel_features+" + ], + [ + "aspect_rules_js+", + "bazel_lib", + "bazel_lib+" + ], + [ + "aspect_rules_js+", + "bazel_skylib", + "bazel_skylib+" + ], + [ + "aspect_rules_js+", + "bazel_tools", + "bazel_tools" + ], + [ + "bazel_features+", + "bazel_features_globals", + "bazel_features++version_extension+bazel_features_globals" + ], + [ + "bazel_features+", + "bazel_features_version", + "bazel_features++version_extension+bazel_features_version" + ], + [ + "bazel_lib+", + "bazel_skylib", + "bazel_skylib+" + ], + [ + "bazel_lib+", + "bazel_tools", + "bazel_tools" + ], + [ + "tar.bzl+", + "aspect_bazel_lib", + "aspect_bazel_lib+" + ], + [ + "tar.bzl+", + "bazel_skylib", + "bazel_skylib+" + ], + [ + "tar.bzl+", + "tar.bzl", + "tar.bzl+" + ] + ] + } + }, "@@aspect_tools_telemetry+//:extension.bzl%telemetry": { "general": { "bzlTransitiveDigest": "cl5A2O84vDL6Tt+Qga8FCj1DUDGqn+e7ly5rZ+4xvcc=", @@ -206,6 +360,123 @@ ] } }, + "@@pybind11_bazel+//:python_configure.bzl%extension": { + "general": { + "bzlTransitiveDigest": "d4N/SZrl3ONcmzE98rcV0Fsro0iUbjNQFTIiLiGuH+k=", + "usagesDigest": "fycyB39YnXIJkfWCIXLUKJMZzANcuLy9ZE73hRucjFk=", + "recordedFileInputs": { + "@@pybind11_bazel+//MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e" + }, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "local_config_python": { + "repoRuleId": "@@pybind11_bazel+//:python_configure.bzl%python_configure", + "attributes": {} + }, + "pybind11": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file": "@@pybind11_bazel+//:pybind11.BUILD", + "strip_prefix": "pybind11-2.11.1", + "urls": [ + "https://github.com/pybind/pybind11/archive/v2.11.1.zip" + ] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "pybind11_bazel+", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, + "@@rules_fuzzing+//fuzzing/private:extensions.bzl%non_module_dependencies": { + "general": { + "bzlTransitiveDigest": "mGiTB79hRNjmeDTQdzkpCHyzXhErMbufeAmySBt7s5s=", + "usagesDigest": "wy6ISK6UOcBEjj/mvJ/S3WeXoO67X+1llb9yPyFtPgc=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "platforms": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "urls": [ + "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz", + "https://github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz" + ], + "sha256": "8150406605389ececb6da07cbcb509d5637a3ab9a24bc69b1101531367d89d74" + } + }, + "rules_python": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "d70cd72a7a4880f0000a6346253414825c19cdd40a28289bdf67b8e6480edff8", + "strip_prefix": "rules_python-0.28.0", + "url": "https://github.com/bazelbuild/rules_python/releases/download/0.28.0/rules_python-0.28.0.tar.gz" + } + }, + "bazel_skylib": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "cd55a062e763b9349921f0f5db8c3933288dc8ba4f76dd9416aac68acee3cb94", + "urls": [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz" + ] + } + }, + "com_google_absl": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "urls": [ + "https://github.com/abseil/abseil-cpp/archive/refs/tags/20240116.1.zip" + ], + "strip_prefix": "abseil-cpp-20240116.1", + "integrity": "sha256-7capMWOvWyoYbUaHF/b+I2U6XLMaHmky8KugWvfXYuk=" + } + }, + "rules_fuzzing_oss_fuzz": { + "repoRuleId": "@@rules_fuzzing+//fuzzing/private/oss_fuzz:repository.bzl%oss_fuzz_repository", + "attributes": {} + }, + "honggfuzz": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file": "@@rules_fuzzing+//:honggfuzz.BUILD", + "sha256": "6b18ba13bc1f36b7b950c72d80f19ea67fbadc0ac0bb297ec89ad91f2eaa423e", + "url": "https://github.com/google/honggfuzz/archive/2.5.zip", + "strip_prefix": "honggfuzz-2.5" + } + }, + "rules_fuzzing_jazzer": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar", + "attributes": { + "sha256": "ee6feb569d88962d59cb59e8a31eb9d007c82683f3ebc64955fd5b96f277eec2", + "url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer/0.20.1/jazzer-0.20.1.jar" + } + }, + "rules_fuzzing_jazzer_api": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_jar", + "attributes": { + "sha256": "f5a60242bc408f7fa20fccf10d6c5c5ea1fcb3c6f44642fec5af88373ae7aa1b", + "url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer-api/0.20.1/jazzer-api-0.20.1.jar" + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_fuzzing+", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "@@rules_java+//java:rules_java_deps.bzl%compatibility_proxy": { "general": { "bzlTransitiveDigest": "84xJEZ1jnXXwo8BXMprvBm++rRt4jsTu9liBxz0ivps=", @@ -426,6 +697,2500 @@ "recordedRepoMappingEntries": [] } }, + "@@rules_python+//python/private/pypi:pip.bzl%pip_internal": { + "general": { + "bzlTransitiveDigest": "LoTaMl3T/Mr0pZwTYVMcRc5gv6qNfnE8umRCDYy1m7U=", + "usagesDigest": "OLoIStnzNObNalKEMRq99FqenhPGLFZ5utVLV4sz7OI=", + "recordedFileInputs": { + "@@rules_python+//tools/publish/requirements_darwin.txt": "2994136eab7e57b083c3de76faf46f70fad130bc8e7360a7fed2b288b69e79dc", + "@@rules_python+//tools/publish/requirements_linux.txt": "8175b4c8df50ae2f22d1706961884beeb54e7da27bd2447018314a175981997d", + "@@rules_python+//tools/publish/requirements_windows.txt": "7673adc71dc1a81d3661b90924d7a7c0fc998cd508b3cb4174337cef3f2de556" + }, + "recordedDirentsInputs": {}, + "envVariables": { + "RULES_PYTHON_REPO_DEBUG": null, + "RULES_PYTHON_REPO_DEBUG_VERBOSITY": null + }, + "generatedRepoSpecs": { + "rules_python_publish_deps_311_backports_tarfile_py3_none_any_77e284d7": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "backports.tarfile-1.2.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "backports-tarfile==1.2.0", + "sha256": "77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", + "urls": [ + "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_backports_tarfile_sdist_d75e02c2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "backports_tarfile-1.2.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "backports-tarfile==1.2.0", + "sha256": "d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", + "urls": [ + "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_certifi_py3_none_any_922820b5": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "certifi-2024.8.30-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "certifi==2024.8.30", + "sha256": "922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "urls": [ + "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_certifi_sdist_bec941d2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "certifi-2024.8.30.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "certifi==2024.8.30", + "sha256": "bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", + "urls": [ + "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_aarch64_a1ed2dd2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "urls": [ + "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_ppc64le_46bf4316": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "urls": [ + "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_s390x_a24ed04c": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "urls": [ + "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_x86_64_610faea7": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "urls": [ + "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_aarch64_a9b15d49": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "urls": [ + "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_x86_64_fc48c783": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", + "urls": [ + "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cffi_sdist_1c39c601": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "cffi-1.17.1.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cffi==1.17.1", + "sha256": "1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "urls": [ + "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_universal2_0d99dd8f": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "urls": [ + "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_x86_64_c57516e5": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "urls": [ + "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_11_0_arm64_6dba5d19": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "urls": [ + "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_aarch64_bf4475b8": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "urls": [ + "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_ppc64le_ce031db0": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "urls": [ + "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_s390x_8ff4e7cd": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "urls": [ + "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_x86_64_3710a975": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "urls": [ + "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_aarch64_47334db7": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "urls": [ + "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_ppc64le_f1a2f519": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "urls": [ + "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_s390x_63bc5c4a": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "urls": [ + "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_x86_64_bcb4f8ea": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "urls": [ + "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_cp311_cp311_win_amd64_cee4373f": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "urls": [ + "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_py3_none_any_fe9f97fe": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "charset_normalizer-3.4.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "urls": [ + "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_charset_normalizer_sdist_223217c3": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "charset_normalizer-3.4.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "charset-normalizer==3.4.0", + "sha256": "223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "urls": [ + "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_aarch64_846da004": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", + "urls": [ + "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_x86_64_0f996e72": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", + "urls": [ + "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_aarch64_f7b178f1": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", + "urls": [ + "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_x86_64_c2e6fc39": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", + "urls": [ + "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_aarch64_e1be4655": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", + "urls": [ + "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_x86_64_df6b6c6d": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", + "urls": [ + "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_cryptography_sdist_315b9001": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "cryptography-43.0.3.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "cryptography==43.0.3", + "sha256": "315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", + "urls": [ + "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_docutils_py3_none_any_dafca5b9": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "docutils-0.21.2-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "docutils==0.21.2", + "sha256": "dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", + "urls": [ + "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_docutils_sdist_3a6b1873": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "docutils-0.21.2.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "docutils==0.21.2", + "sha256": "3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", + "urls": [ + "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_idna_py3_none_any_946d195a": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "idna-3.10-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "idna==3.10", + "sha256": "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", + "urls": [ + "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_idna_sdist_12f65c9b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "idna-3.10.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "idna==3.10", + "sha256": "12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "urls": [ + "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_importlib_metadata_py3_none_any_45e54197": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "importlib_metadata-8.5.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "importlib-metadata==8.5.0", + "sha256": "45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", + "urls": [ + "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_importlib_metadata_sdist_71522656": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "importlib_metadata-8.5.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "importlib-metadata==8.5.0", + "sha256": "71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", + "urls": [ + "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jaraco_classes_py3_none_any_f662826b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "jaraco.classes-3.4.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-classes==3.4.0", + "sha256": "f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", + "urls": [ + "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jaraco_classes_sdist_47a024b5": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jaraco.classes-3.4.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-classes==3.4.0", + "sha256": "47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", + "urls": [ + "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jaraco_context_py3_none_any_f797fc48": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "jaraco.context-6.0.1-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-context==6.0.1", + "sha256": "f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", + "urls": [ + "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jaraco_context_sdist_9bae4ea5": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jaraco_context-6.0.1.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-context==6.0.1", + "sha256": "9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", + "urls": [ + "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jaraco_functools_py3_none_any_ad159f13": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "jaraco.functools-4.1.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-functools==4.1.0", + "sha256": "ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", + "urls": [ + "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jaraco_functools_sdist_70f7e0e2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jaraco_functools-4.1.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jaraco-functools==4.1.0", + "sha256": "70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", + "urls": [ + "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_jeepney_py3_none_any_c0a454ad": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "jeepney-0.8.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jeepney==0.8.0", + "sha256": "c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", + "urls": [ + "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_jeepney_sdist_5efe48d2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "jeepney-0.8.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "jeepney==0.8.0", + "sha256": "5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", + "urls": [ + "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_keyring_py3_none_any_5426f817": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "keyring-25.4.1-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "keyring==25.4.1", + "sha256": "5426f817cf7f6f007ba5ec722b1bcad95a75b27d780343772ad76b17cb47b0bf", + "urls": [ + "https://files.pythonhosted.org/packages/83/25/e6d59e5f0a0508d0dca8bb98c7f7fd3772fc943ac3f53d5ab18a218d32c0/keyring-25.4.1-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_keyring_sdist_b07ebc55": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "keyring-25.4.1.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "keyring==25.4.1", + "sha256": "b07ebc55f3e8ed86ac81dd31ef14e81ace9dd9c3d4b5d77a6e9a2016d0d71a1b", + "urls": [ + "https://files.pythonhosted.org/packages/a5/1c/2bdbcfd5d59dc6274ffb175bc29aa07ecbfab196830e0cfbde7bd861a2ea/keyring-25.4.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_markdown_it_py_py3_none_any_35521684": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "markdown_it_py-3.0.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "markdown-it-py==3.0.0", + "sha256": "355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", + "urls": [ + "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_markdown_it_py_sdist_e3f60a94": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "markdown-it-py-3.0.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "markdown-it-py==3.0.0", + "sha256": "e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", + "urls": [ + "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_mdurl_py3_none_any_84008a41": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "mdurl-0.1.2-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "mdurl==0.1.2", + "sha256": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "urls": [ + "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_mdurl_sdist_bb413d29": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "mdurl-0.1.2.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "mdurl==0.1.2", + "sha256": "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", + "urls": [ + "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_more_itertools_py3_none_any_037b0d32": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "more_itertools-10.5.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "more-itertools==10.5.0", + "sha256": "037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", + "urls": [ + "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_more_itertools_sdist_5482bfef": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "more-itertools-10.5.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "more-itertools==10.5.0", + "sha256": "5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", + "urls": [ + "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_14c5a72e": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", + "urls": [ + "https://files.pythonhosted.org/packages/b3/89/1daff5d9ba5a95a157c092c7c5f39b8dd2b1ddb4559966f808d31cfb67e0/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_7b7c2a3c": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", + "urls": [ + "https://files.pythonhosted.org/packages/2c/b6/42fc3c69cabf86b6b81e4c051a9b6e249c5ba9f8155590222c2622961f58/nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_aarch64_42c64511": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", + "urls": [ + "https://files.pythonhosted.org/packages/45/b9/833f385403abaf0023c6547389ec7a7acf141ddd9d1f21573723a6eab39a/nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_armv7l_0411beb0": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", + "urls": [ + "https://files.pythonhosted.org/packages/05/2b/85977d9e11713b5747595ee61f381bc820749daf83f07b90b6c9964cf932/nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_ppc64_5f36b271": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", + "urls": [ + "https://files.pythonhosted.org/packages/72/f2/5c894d5265ab80a97c68ca36f25c8f6f0308abac649aaf152b74e7e854a8/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_ppc64le_34c03fa7": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", + "urls": [ + "https://files.pythonhosted.org/packages/ab/a7/375afcc710dbe2d64cfbd69e31f82f3e423d43737258af01f6a56d844085/nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_s390x_19aaba96": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", + "urls": [ + "https://files.pythonhosted.org/packages/c2/a8/3bb02d0c60a03ad3a112b76c46971e9480efa98a8946677b5a59f60130ca/nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_x86_64_de3ceed6": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", + "urls": [ + "https://files.pythonhosted.org/packages/1b/63/6ab90d0e5225ab9780f6c9fb52254fa36b52bb7c188df9201d05b647e5e1/nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_aarch64_f0eca9ca": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe", + "urls": [ + "https://files.pythonhosted.org/packages/a3/da/0c4e282bc3cff4a0adf37005fa1fb42257673fbc1bbf7d1ff639ec3d255a/nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_armv7l_3a157ab1": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", + "urls": [ + "https://files.pythonhosted.org/packages/de/81/c291231463d21da5f8bba82c8167a6d6893cc5419b0639801ee5d3aeb8a9/nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_x86_64_36c95d4b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", + "urls": [ + "https://files.pythonhosted.org/packages/eb/61/73a007c74c37895fdf66e0edcd881f5eaa17a348ff02f4bb4bc906d61085/nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_cp37_abi3_win_amd64_8ce0f819": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "nh3-0.2.18-cp37-abi3-win_amd64.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", + "urls": [ + "https://files.pythonhosted.org/packages/26/8d/53c5b19c4999bdc6ba95f246f4ef35ca83d7d7423e5e38be43ad66544e5d/nh3-0.2.18-cp37-abi3-win_amd64.whl" + ] + } + }, + "rules_python_publish_deps_311_nh3_sdist_94a16692": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "nh3-0.2.18.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "nh3==0.2.18", + "sha256": "94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", + "urls": [ + "https://files.pythonhosted.org/packages/62/73/10df50b42ddb547a907deeb2f3c9823022580a7a47281e8eae8e003a9639/nh3-0.2.18.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pkginfo_py3_none_any_889a6da2": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "pkginfo-1.10.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pkginfo==1.10.0", + "sha256": "889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097", + "urls": [ + "https://files.pythonhosted.org/packages/56/09/054aea9b7534a15ad38a363a2bd974c20646ab1582a387a95b8df1bfea1c/pkginfo-1.10.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pkginfo_sdist_5df73835": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pkginfo-1.10.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pkginfo==1.10.0", + "sha256": "5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", + "urls": [ + "https://files.pythonhosted.org/packages/2f/72/347ec5be4adc85c182ed2823d8d1c7b51e13b9a6b0c1aae59582eca652df/pkginfo-1.10.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pycparser_py3_none_any_c3702b6d": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "pycparser-2.22-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pycparser==2.22", + "sha256": "c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", + "urls": [ + "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pycparser_sdist_491c8be9": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pycparser-2.22.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pycparser==2.22", + "sha256": "491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", + "urls": [ + "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pygments_py3_none_any_b8e6aca0": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "pygments-2.18.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pygments==2.18.0", + "sha256": "b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", + "urls": [ + "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pygments_sdist_786ff802": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pygments-2.18.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pygments==2.18.0", + "sha256": "786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "urls": [ + "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_pywin32_ctypes_py3_none_any_8a151337": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_windows_x86_64" + ], + "filename": "pywin32_ctypes-0.2.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pywin32-ctypes==0.2.3", + "sha256": "8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", + "urls": [ + "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_pywin32_ctypes_sdist_d162dc04": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "pywin32-ctypes-0.2.3.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "pywin32-ctypes==0.2.3", + "sha256": "d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", + "urls": [ + "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_readme_renderer_py3_none_any_2fbca89b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "readme_renderer-44.0-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "readme-renderer==44.0", + "sha256": "2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", + "urls": [ + "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_readme_renderer_sdist_8712034e": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "readme_renderer-44.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "readme-renderer==44.0", + "sha256": "8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", + "urls": [ + "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_requests_py3_none_any_70761cfe": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "requests-2.32.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests==2.32.3", + "sha256": "70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", + "urls": [ + "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_requests_sdist_55365417": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "requests-2.32.3.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests==2.32.3", + "sha256": "55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", + "urls": [ + "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_requests_toolbelt_py2_none_any_cccfdd66": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "requests_toolbelt-1.0.0-py2.py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests-toolbelt==1.0.0", + "sha256": "cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", + "urls": [ + "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_requests_toolbelt_sdist_7681a0a3": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "requests-toolbelt-1.0.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "requests-toolbelt==1.0.0", + "sha256": "7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", + "urls": [ + "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_rfc3986_py2_none_any_50b1502b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "rfc3986-2.0.0-py2.py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rfc3986==2.0.0", + "sha256": "50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", + "urls": [ + "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_rfc3986_sdist_97aacf9d": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "rfc3986-2.0.0.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rfc3986==2.0.0", + "sha256": "97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", + "urls": [ + "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_rich_py3_none_any_9836f509": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "rich-13.9.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rich==13.9.3", + "sha256": "9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283", + "urls": [ + "https://files.pythonhosted.org/packages/9a/e2/10e9819cf4a20bd8ea2f5dabafc2e6bf4a78d6a0965daeb60a4b34d1c11f/rich-13.9.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_rich_sdist_bc1e01b8": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "rich-13.9.3.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "rich==13.9.3", + "sha256": "bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e", + "urls": [ + "https://files.pythonhosted.org/packages/d9/e9/cf9ef5245d835065e6673781dbd4b8911d352fb770d56cf0879cf11b7ee1/rich-13.9.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_secretstorage_py3_none_any_f356e662": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "filename": "SecretStorage-3.3.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "secretstorage==3.3.3", + "sha256": "f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", + "urls": [ + "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_secretstorage_sdist_2403533e": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "SecretStorage-3.3.3.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "secretstorage==3.3.3", + "sha256": "2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", + "urls": [ + "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_twine_py3_none_any_215dbe7b": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "twine-5.1.1-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "twine==5.1.1", + "sha256": "215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", + "urls": [ + "https://files.pythonhosted.org/packages/5d/ec/00f9d5fd040ae29867355e559a94e9a8429225a0284a3f5f091a3878bfc0/twine-5.1.1-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_twine_sdist_9aa08251": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "twine-5.1.1.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "twine==5.1.1", + "sha256": "9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db", + "urls": [ + "https://files.pythonhosted.org/packages/77/68/bd982e5e949ef8334e6f7dcf76ae40922a8750aa2e347291ae1477a4782b/twine-5.1.1.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_urllib3_py3_none_any_ca899ca0": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "urllib3-2.2.3-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "urllib3==2.2.3", + "sha256": "ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "urls": [ + "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_urllib3_sdist_e7d814a8": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "urllib3-2.2.3.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "urllib3==2.2.3", + "sha256": "e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", + "urls": [ + "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz" + ] + } + }, + "rules_python_publish_deps_311_zipp_py3_none_any_a817ac80": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "filename": "zipp-3.20.2-py3-none-any.whl", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "zipp==3.20.2", + "sha256": "a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", + "urls": [ + "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl" + ] + } + }, + "rules_python_publish_deps_311_zipp_sdist_bc9eb26f": { + "repoRuleId": "@@rules_python+//python/private/pypi:whl_library.bzl%whl_library", + "attributes": { + "dep_template": "@rules_python_publish_deps//{name}:{target}", + "experimental_target_platforms": [ + "cp311_linux_aarch64", + "cp311_linux_arm", + "cp311_linux_ppc", + "cp311_linux_s390x", + "cp311_linux_x86_64", + "cp311_osx_aarch64", + "cp311_osx_x86_64", + "cp311_windows_x86_64" + ], + "extra_pip_args": [ + "--index-url", + "https://pypi.org/simple" + ], + "filename": "zipp-3.20.2.tar.gz", + "python_interpreter_target": "@@rules_python++python+python_3_11_host//:python", + "repo": "rules_python_publish_deps_311", + "requirement": "zipp==3.20.2", + "sha256": "bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", + "urls": [ + "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz" + ] + } + }, + "rules_python_publish_deps": { + "repoRuleId": "@@rules_python+//python/private/pypi:hub_repository.bzl%hub_repository", + "attributes": { + "repo_name": "rules_python_publish_deps", + "extra_hub_aliases": {}, + "whl_map": { + "backports_tarfile": "[{\"filename\":\"backports.tarfile-1.2.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_backports_tarfile_py3_none_any_77e284d7\",\"version\":\"3.11\"},{\"filename\":\"backports_tarfile-1.2.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_backports_tarfile_sdist_d75e02c2\",\"version\":\"3.11\"}]", + "certifi": "[{\"filename\":\"certifi-2024.8.30-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_certifi_py3_none_any_922820b5\",\"version\":\"3.11\"},{\"filename\":\"certifi-2024.8.30.tar.gz\",\"repo\":\"rules_python_publish_deps_311_certifi_sdist_bec941d2\",\"version\":\"3.11\"}]", + "cffi": "[{\"filename\":\"cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"repo\":\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_aarch64_a1ed2dd2\",\"version\":\"3.11\"},{\"filename\":\"cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl\",\"repo\":\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_ppc64le_46bf4316\",\"version\":\"3.11\"},{\"filename\":\"cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl\",\"repo\":\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_s390x_a24ed04c\",\"version\":\"3.11\"},{\"filename\":\"cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_cffi_cp311_cp311_manylinux_2_17_x86_64_610faea7\",\"version\":\"3.11\"},{\"filename\":\"cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl\",\"repo\":\"rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_aarch64_a9b15d49\",\"version\":\"3.11\"},{\"filename\":\"cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_cffi_cp311_cp311_musllinux_1_1_x86_64_fc48c783\",\"version\":\"3.11\"},{\"filename\":\"cffi-1.17.1.tar.gz\",\"repo\":\"rules_python_publish_deps_311_cffi_sdist_1c39c601\",\"version\":\"3.11\"}]", + "charset_normalizer": "[{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_universal2_0d99dd8f\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_10_9_x86_64_c57516e5\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_macosx_11_0_arm64_6dba5d19\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_aarch64_bf4475b8\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_ppc64le_ce031db0\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_s390x_8ff4e7cd\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_manylinux_2_17_x86_64_3710a975\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_aarch64_47334db7\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_ppc64le_f1a2f519\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_s390x_63bc5c4a\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_musllinux_1_2_x86_64_bcb4f8ea\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_cp311_cp311_win_amd64_cee4373f\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_py3_none_any_fe9f97fe\",\"version\":\"3.11\"},{\"filename\":\"charset_normalizer-3.4.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_charset_normalizer_sdist_223217c3\",\"version\":\"3.11\"}]", + "cryptography": "[{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"repo\":\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_aarch64_846da004\",\"version\":\"3.11\"},{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_17_x86_64_0f996e72\",\"version\":\"3.11\"},{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl\",\"repo\":\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_aarch64_f7b178f1\",\"version\":\"3.11\"},{\"filename\":\"cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_cryptography_cp39_abi3_manylinux_2_28_x86_64_c2e6fc39\",\"version\":\"3.11\"},{\"filename\":\"cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl\",\"repo\":\"rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_aarch64_e1be4655\",\"version\":\"3.11\"},{\"filename\":\"cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_cryptography_cp39_abi3_musllinux_1_2_x86_64_df6b6c6d\",\"version\":\"3.11\"},{\"filename\":\"cryptography-43.0.3.tar.gz\",\"repo\":\"rules_python_publish_deps_311_cryptography_sdist_315b9001\",\"version\":\"3.11\"}]", + "docutils": "[{\"filename\":\"docutils-0.21.2-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_docutils_py3_none_any_dafca5b9\",\"version\":\"3.11\"},{\"filename\":\"docutils-0.21.2.tar.gz\",\"repo\":\"rules_python_publish_deps_311_docutils_sdist_3a6b1873\",\"version\":\"3.11\"}]", + "idna": "[{\"filename\":\"idna-3.10-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_idna_py3_none_any_946d195a\",\"version\":\"3.11\"},{\"filename\":\"idna-3.10.tar.gz\",\"repo\":\"rules_python_publish_deps_311_idna_sdist_12f65c9b\",\"version\":\"3.11\"}]", + "importlib_metadata": "[{\"filename\":\"importlib_metadata-8.5.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_importlib_metadata_py3_none_any_45e54197\",\"version\":\"3.11\"},{\"filename\":\"importlib_metadata-8.5.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_importlib_metadata_sdist_71522656\",\"version\":\"3.11\"}]", + "jaraco_classes": "[{\"filename\":\"jaraco.classes-3.4.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_jaraco_classes_py3_none_any_f662826b\",\"version\":\"3.11\"},{\"filename\":\"jaraco.classes-3.4.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_jaraco_classes_sdist_47a024b5\",\"version\":\"3.11\"}]", + "jaraco_context": "[{\"filename\":\"jaraco.context-6.0.1-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_jaraco_context_py3_none_any_f797fc48\",\"version\":\"3.11\"},{\"filename\":\"jaraco_context-6.0.1.tar.gz\",\"repo\":\"rules_python_publish_deps_311_jaraco_context_sdist_9bae4ea5\",\"version\":\"3.11\"}]", + "jaraco_functools": "[{\"filename\":\"jaraco.functools-4.1.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_jaraco_functools_py3_none_any_ad159f13\",\"version\":\"3.11\"},{\"filename\":\"jaraco_functools-4.1.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_jaraco_functools_sdist_70f7e0e2\",\"version\":\"3.11\"}]", + "jeepney": "[{\"filename\":\"jeepney-0.8.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_jeepney_py3_none_any_c0a454ad\",\"version\":\"3.11\"},{\"filename\":\"jeepney-0.8.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_jeepney_sdist_5efe48d2\",\"version\":\"3.11\"}]", + "keyring": "[{\"filename\":\"keyring-25.4.1-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_keyring_py3_none_any_5426f817\",\"version\":\"3.11\"},{\"filename\":\"keyring-25.4.1.tar.gz\",\"repo\":\"rules_python_publish_deps_311_keyring_sdist_b07ebc55\",\"version\":\"3.11\"}]", + "markdown_it_py": "[{\"filename\":\"markdown-it-py-3.0.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_markdown_it_py_sdist_e3f60a94\",\"version\":\"3.11\"},{\"filename\":\"markdown_it_py-3.0.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_markdown_it_py_py3_none_any_35521684\",\"version\":\"3.11\"}]", + "mdurl": "[{\"filename\":\"mdurl-0.1.2-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_mdurl_py3_none_any_84008a41\",\"version\":\"3.11\"},{\"filename\":\"mdurl-0.1.2.tar.gz\",\"repo\":\"rules_python_publish_deps_311_mdurl_sdist_bb413d29\",\"version\":\"3.11\"}]", + "more_itertools": "[{\"filename\":\"more-itertools-10.5.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_more_itertools_sdist_5482bfef\",\"version\":\"3.11\"},{\"filename\":\"more_itertools-10.5.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_more_itertools_py3_none_any_037b0d32\",\"version\":\"3.11\"}]", + "nh3": "[{\"filename\":\"nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_14c5a72e\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18-cp37-abi3-macosx_10_12_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_macosx_10_12_x86_64_7b7c2a3c\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_aarch64_42c64511\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_armv7l_0411beb0\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_ppc64_5f36b271\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_ppc64le_34c03fa7\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_s390x_19aaba96\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_manylinux_2_17_x86_64_de3ceed6\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18-cp37-abi3-musllinux_1_2_aarch64.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_aarch64_f0eca9ca\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18-cp37-abi3-musllinux_1_2_armv7l.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_armv7l_3a157ab1\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18-cp37-abi3-musllinux_1_2_x86_64.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_musllinux_1_2_x86_64_36c95d4b\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18-cp37-abi3-win_amd64.whl\",\"repo\":\"rules_python_publish_deps_311_nh3_cp37_abi3_win_amd64_8ce0f819\",\"version\":\"3.11\"},{\"filename\":\"nh3-0.2.18.tar.gz\",\"repo\":\"rules_python_publish_deps_311_nh3_sdist_94a16692\",\"version\":\"3.11\"}]", + "pkginfo": "[{\"filename\":\"pkginfo-1.10.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_pkginfo_py3_none_any_889a6da2\",\"version\":\"3.11\"},{\"filename\":\"pkginfo-1.10.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_pkginfo_sdist_5df73835\",\"version\":\"3.11\"}]", + "pycparser": "[{\"filename\":\"pycparser-2.22-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_pycparser_py3_none_any_c3702b6d\",\"version\":\"3.11\"},{\"filename\":\"pycparser-2.22.tar.gz\",\"repo\":\"rules_python_publish_deps_311_pycparser_sdist_491c8be9\",\"version\":\"3.11\"}]", + "pygments": "[{\"filename\":\"pygments-2.18.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_pygments_py3_none_any_b8e6aca0\",\"version\":\"3.11\"},{\"filename\":\"pygments-2.18.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_pygments_sdist_786ff802\",\"version\":\"3.11\"}]", + "pywin32_ctypes": "[{\"filename\":\"pywin32-ctypes-0.2.3.tar.gz\",\"repo\":\"rules_python_publish_deps_311_pywin32_ctypes_sdist_d162dc04\",\"version\":\"3.11\"},{\"filename\":\"pywin32_ctypes-0.2.3-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_pywin32_ctypes_py3_none_any_8a151337\",\"version\":\"3.11\"}]", + "readme_renderer": "[{\"filename\":\"readme_renderer-44.0-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_readme_renderer_py3_none_any_2fbca89b\",\"version\":\"3.11\"},{\"filename\":\"readme_renderer-44.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_readme_renderer_sdist_8712034e\",\"version\":\"3.11\"}]", + "requests": "[{\"filename\":\"requests-2.32.3-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_requests_py3_none_any_70761cfe\",\"version\":\"3.11\"},{\"filename\":\"requests-2.32.3.tar.gz\",\"repo\":\"rules_python_publish_deps_311_requests_sdist_55365417\",\"version\":\"3.11\"}]", + "requests_toolbelt": "[{\"filename\":\"requests-toolbelt-1.0.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_requests_toolbelt_sdist_7681a0a3\",\"version\":\"3.11\"},{\"filename\":\"requests_toolbelt-1.0.0-py2.py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_requests_toolbelt_py2_none_any_cccfdd66\",\"version\":\"3.11\"}]", + "rfc3986": "[{\"filename\":\"rfc3986-2.0.0-py2.py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_rfc3986_py2_none_any_50b1502b\",\"version\":\"3.11\"},{\"filename\":\"rfc3986-2.0.0.tar.gz\",\"repo\":\"rules_python_publish_deps_311_rfc3986_sdist_97aacf9d\",\"version\":\"3.11\"}]", + "rich": "[{\"filename\":\"rich-13.9.3-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_rich_py3_none_any_9836f509\",\"version\":\"3.11\"},{\"filename\":\"rich-13.9.3.tar.gz\",\"repo\":\"rules_python_publish_deps_311_rich_sdist_bc1e01b8\",\"version\":\"3.11\"}]", + "secretstorage": "[{\"filename\":\"SecretStorage-3.3.3-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_secretstorage_py3_none_any_f356e662\",\"version\":\"3.11\"},{\"filename\":\"SecretStorage-3.3.3.tar.gz\",\"repo\":\"rules_python_publish_deps_311_secretstorage_sdist_2403533e\",\"version\":\"3.11\"}]", + "twine": "[{\"filename\":\"twine-5.1.1-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_twine_py3_none_any_215dbe7b\",\"version\":\"3.11\"},{\"filename\":\"twine-5.1.1.tar.gz\",\"repo\":\"rules_python_publish_deps_311_twine_sdist_9aa08251\",\"version\":\"3.11\"}]", + "urllib3": "[{\"filename\":\"urllib3-2.2.3-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_urllib3_py3_none_any_ca899ca0\",\"version\":\"3.11\"},{\"filename\":\"urllib3-2.2.3.tar.gz\",\"repo\":\"rules_python_publish_deps_311_urllib3_sdist_e7d814a8\",\"version\":\"3.11\"}]", + "zipp": "[{\"filename\":\"zipp-3.20.2-py3-none-any.whl\",\"repo\":\"rules_python_publish_deps_311_zipp_py3_none_any_a817ac80\",\"version\":\"3.11\"},{\"filename\":\"zipp-3.20.2.tar.gz\",\"repo\":\"rules_python_publish_deps_311_zipp_sdist_bc9eb26f\",\"version\":\"3.11\"}]" + }, + "packages": [ + "backports_tarfile", + "certifi", + "charset_normalizer", + "docutils", + "idna", + "importlib_metadata", + "jaraco_classes", + "jaraco_context", + "jaraco_functools", + "keyring", + "markdown_it_py", + "mdurl", + "more_itertools", + "nh3", + "pkginfo", + "pygments", + "readme_renderer", + "requests", + "requests_toolbelt", + "rfc3986", + "rich", + "twine", + "urllib3", + "zipp" + ], + "groups": {} + } + } + }, + "recordedRepoMappingEntries": [ + [ + "bazel_features+", + "bazel_features_globals", + "bazel_features++version_extension+bazel_features_globals" + ], + [ + "bazel_features+", + "bazel_features_version", + "bazel_features++version_extension+bazel_features_version" + ], + [ + "rules_python+", + "bazel_features", + "bazel_features+" + ], + [ + "rules_python+", + "bazel_skylib", + "bazel_skylib+" + ], + [ + "rules_python+", + "bazel_tools", + "bazel_tools" + ], + [ + "rules_python+", + "pypi__build", + "rules_python++internal_deps+pypi__build" + ], + [ + "rules_python+", + "pypi__click", + "rules_python++internal_deps+pypi__click" + ], + [ + "rules_python+", + "pypi__colorama", + "rules_python++internal_deps+pypi__colorama" + ], + [ + "rules_python+", + "pypi__importlib_metadata", + "rules_python++internal_deps+pypi__importlib_metadata" + ], + [ + "rules_python+", + "pypi__installer", + "rules_python++internal_deps+pypi__installer" + ], + [ + "rules_python+", + "pypi__more_itertools", + "rules_python++internal_deps+pypi__more_itertools" + ], + [ + "rules_python+", + "pypi__packaging", + "rules_python++internal_deps+pypi__packaging" + ], + [ + "rules_python+", + "pypi__pep517", + "rules_python++internal_deps+pypi__pep517" + ], + [ + "rules_python+", + "pypi__pip", + "rules_python++internal_deps+pypi__pip" + ], + [ + "rules_python+", + "pypi__pip_tools", + "rules_python++internal_deps+pypi__pip_tools" + ], + [ + "rules_python+", + "pypi__pyproject_hooks", + "rules_python++internal_deps+pypi__pyproject_hooks" + ], + [ + "rules_python+", + "pypi__setuptools", + "rules_python++internal_deps+pypi__setuptools" + ], + [ + "rules_python+", + "pypi__tomli", + "rules_python++internal_deps+pypi__tomli" + ], + [ + "rules_python+", + "pypi__wheel", + "rules_python++internal_deps+pypi__wheel" + ], + [ + "rules_python+", + "pypi__zipp", + "rules_python++internal_deps+pypi__zipp" + ], + [ + "rules_python+", + "pythons_hub", + "rules_python++python+pythons_hub" + ], + [ + "rules_python++python+pythons_hub", + "python_3_10_host", + "rules_python++python+python_3_10_host" + ], + [ + "rules_python++python+pythons_hub", + "python_3_11_host", + "rules_python++python+python_3_11_host" + ], + [ + "rules_python++python+pythons_hub", + "python_3_12_host", + "rules_python++python+python_3_12_host" + ], + [ + "rules_python++python+pythons_hub", + "python_3_8_host", + "rules_python++python+python_3_8_host" + ], + [ + "rules_python++python+pythons_hub", + "python_3_9_host", + "rules_python++python+python_3_9_host" + ] + ] + } + }, "@@tar.bzl+//tar:extensions.bzl%toolchains": { "general": { "bzlTransitiveDigest": "/2afh6fPjq/rcyE/jztQDK3ierehmFFngfvmqyRv72M=", diff --git a/README.md b/README.md index fa64161..3b8de5a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ pnpm add @tummycrypt/tinyvectors Peer dependency: -- `svelte@^5` +- `svelte@>=5.20.0` ## Quick Start @@ -40,6 +40,40 @@ Peer dependency: ``` +Device motion must be requested from a user gesture on browsers that gate sensor APIs: + +```svelte + + + + + +``` + +TinyVectors auto-starts device-orientation motion on secure browsers that do not require a +permission prompt. On permission-gated browsers, keep `enableDeviceMotion={true}` and call +`requestDeviceMotionPermission()` from a user gesture. If sensor events pause or the document is +hidden, TinyVectors resets device motion to neutral so stale tilt cannot keep steering the blobs. +Tune that watchdog with `deviceMotionIdleResetMs` when a host app needs faster or slower sensor +liveness handling. Pointer physics is enabled by default only when pointer, touch, or mouse input is +detected, and resets to center when pointer input is canceled, the pointer leaves the viewport, or +the window blurs. +Use `getDeviceMotionStatus()` on the component handle to inspect support, permission, and listener +state before deciding whether to show motion-permission UI. + ## Entry Points The package exports these public entry points: @@ -67,8 +101,21 @@ Useful extra commands: - `pnpm dev` runs the local Vite demo app - `pnpm dev:watch` rebuilds the library on change - `pnpm test:pbt` runs the property-based invariants only +- `pnpm test:browser:motion` launches a headless Chrome/CDP probe for synthetic orientation, CDP orientation, directional motion signs, reduced-motion listener lifecycle, and CDP accelerometer input - `pnpm check:release-metadata` verifies `package.json`, `BUILD.bazel`, and `MODULE.bazel` stay aligned - `pnpm check:package` runs `publint` +- `pnpm check:bundle-size` measures the tree-shaken `{ TinyVectors }` consumer bundle with Svelte externalized +- `pnpm check:package-consumer` validates the Bazel-built package from `./bazel-bin/pkg` in a temporary consumer workspace + +The Bazel-to-npm release flow is documented in [docs/release-flow.md](./docs/release-flow.md). +The physics interaction direction is documented in [docs/physics-feel-contract.md](./docs/physics-feel-contract.md). + +The dev app includes a browser/device harness for interaction work: + +- Use the panel toggles to isolate pointer, scroll, and device-motion physics. +- Use `Spoof Tilt` and `Neutral Tilt` to verify TinyVectors motion wiring without relying on browser sensor tooling. +- On a phone or tablet, open the dev URL, tap `Request Motion`, keep the device still, tap `Calibrate`, then tilt the device. +- In desktop Chrome DevTools, use the Sensors panel to emulate orientation changes and watch the motion `x/y/z` status line. The browser probe also exercises Chrome's CDP reduced-motion media emulation and accelerometer override paths. ## Release Truth diff --git a/dev/App.svelte b/dev/App.svelte index 33d3bf0..80b92ba 100644 --- a/dev/App.svelte +++ b/dev/App.svelte @@ -1,25 +1,74 @@
@@ -32,7 +81,9 @@ diff --git a/dev/index.html b/dev/index.html index 614988b..29c36df 100644 --- a/dev/index.html +++ b/dev/index.html @@ -9,38 +9,73 @@ html, body { width: 100%; height: 100%; + min-height: 100vh; + min-height: 100dvh; font-family: system-ui, -apple-system, sans-serif; + overflow: hidden; } + html, body { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); } + html.light, body.light { background: linear-gradient(135deg, #f0f0f0 0%, #e0e0e0 50%, #d0d0d0 100%); } #app { + position: fixed; + inset: 0; width: 100%; - height: 100%; + height: 100vh; + height: 100dvh; } .controls { position: fixed; top: 16px; right: 16px; z-index: 1000; - background: rgba(0, 0, 0, 0.7); + width: min(282px, calc(100vw - 32px)); + max-height: calc(100vh - 32px); + max-height: calc(100dvh - 32px); + overflow: auto; + background: rgba(6, 8, 18, 0.9); + backdrop-filter: blur(14px); padding: 16px; border-radius: 8px; color: white; font-size: 14px; + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.32); } .controls label { - display: block; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 8px; margin-bottom: 8px; } + .controls .checkbox-row { + grid-template-columns: auto minmax(0, 1fr); + justify-content: start; + } + .controls .range-row { + grid-template-columns: minmax(0, 1fr) minmax(118px, 1.2fr) auto; + } .controls select, .controls input { - margin-left: 8px; padding: 4px 8px; border-radius: 4px; border: none; + min-width: 0; + } + .controls select { + max-width: 132px; + } + .controls input[type="range"] { + width: 100%; + } + .controls input[type="checkbox"] { + width: 14px; + height: 14px; + padding: 0; } .controls button { margin-top: 8px; @@ -51,15 +86,36 @@ color: white; cursor: pointer; } + .controls .button-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + } + .controls .button-row button { + margin-top: 0; + padding: 8px 10px; + } + .controls .status { + display: block; + margin-top: 8px; + min-height: 18px; + opacity: 0.75; + } .controls button:hover { background: #4a7de0; } + .hide-controls .controls { + display: none; + } /* Mobile responsive */ - @media (max-width: 480px) { + @media (max-width: 720px) { .controls { top: 8px; - right: 8px; left: 8px; + right: auto; + width: calc(100vw - 16px); + max-height: calc(100vh - 16px); + max-height: calc(100dvh - 16px); padding: 12px; font-size: 12px; } @@ -68,18 +124,22 @@ margin-bottom: 8px; } .controls label { - display: flex; - align-items: center; - justify-content: space-between; margin-bottom: 6px; } - .controls select, .controls input[type="range"] { - max-width: 120px; + .controls select { + width: min(132px, 40vw); + max-width: 132px; + } + .controls .range-row { + grid-template-columns: minmax(0, 1fr) minmax(96px, 36vw) auto; } .controls button { width: 100%; padding: 10px; } + .controls .button-row { + grid-template-columns: 1fr; + } } @@ -88,26 +148,47 @@

TinyVectors Dev

-
diff --git a/dev/main.ts b/dev/main.ts index 8bff2d2..95c9d9b 100644 --- a/dev/main.ts +++ b/dev/main.ts @@ -1,11 +1,98 @@ import { mount, unmount } from 'svelte'; import App from './App.svelte'; +import type { MotionVector } from '../src/motion/DeviceMotion.js'; +import type { TinyVectorsDeviceMotionStatus } from '../src/svelte/types.js'; + +interface DevAppHandle { + requestDeviceMotionPermission: () => Promise; + calibrateDeviceMotion: (samples?: number) => void; + getDeviceMotionStatus: () => TinyVectorsDeviceMotionStatus; +} + +interface DevWindow extends Window { + __tinyvectorsDeviceMotionStatus?: () => TinyVectorsDeviceMotionStatus | null; +} + +const params = new URLSearchParams(window.location.search); +const themes = ['tinyland', 'trans', 'pride'] as const; + +function booleanParam(name: string, fallback: boolean): boolean { + const value = params.get(name); + if (value === null) return fallback; + return !['0', 'false', 'off', 'no'].includes(value.toLowerCase()); +} + +function numberParam(name: string, fallback: number, min: number, max: number): number { + const value = Number(params.get(name)); + if (!Number.isFinite(value)) return fallback; + return Math.max(min, Math.min(max, Math.round(value))); +} + +function themeParam(): (typeof themes)[number] { + const value = params.get('theme'); + return themes.includes(value as (typeof themes)[number]) ? (value as (typeof themes)[number]) : 'tinyland'; +} + +const initialDarkMode = booleanParam('dark', true); +const showControls = booleanParam('controls', true); +const devWindow = window as DevWindow; +document.body.classList.toggle('dark', initialDarkMode); +document.body.classList.toggle('light', !initialDarkMode); +document.body.classList.toggle('hide-controls', !showControls); +document.documentElement.classList.toggle('dark', initialDarkMode); +document.documentElement.classList.toggle('light', !initialDarkMode); + +let app: (ReturnType & DevAppHandle) | null = null; + +function updateMotionStatus(text: string): void { + const motionStatus = document.getElementById('motion-status'); + if (motionStatus) { + motionStatus.textContent = text; + } +} + +function formatMotionSample(sample: MotionVector): string { + const x = sample.x.toFixed(2); + const y = sample.y.toFixed(2); + const z = sample.z.toFixed(2); + return `motion x ${x} y ${y} z ${z}`; +} + +function createDeviceOrientationEvent(alpha: number, beta: number, gamma: number): Event { + if (typeof DeviceOrientationEvent === 'function') { + return new DeviceOrientationEvent('deviceorientation', { + alpha, + beta, + gamma, + absolute: false, + }); + } + + const event = new Event('deviceorientation'); + Object.defineProperties(event, { + alpha: { value: alpha }, + beta: { value: beta }, + gamma: { value: gamma }, + absolute: { value: false }, + }); + return event; +} + +function spoofOrientation(alpha: number, beta: number, gamma: number): void { + window.dispatchEvent(createDeviceOrientationEvent(alpha, beta, gamma)); +} -let app: ReturnType | null = null; let currentProps = { - theme: 'tinyland' as 'tinyland' | 'trans' | 'pride', - blobCount: 12, - animated: true, + theme: themeParam(), + blobCount: numberParam('blobs', 8, 4, 16), + animated: booleanParam('animated', true), + enableDeviceMotion: booleanParam('deviceMotion', true), + enableScrollPhysics: booleanParam('scrollPhysics', true), + enablePointerPhysics: booleanParam('pointerPhysics', true), + deviceMotionIdleResetMs: numberParam('motionIdleReset', 2000, 0, 10000), + onMotionSample(sample: MotionVector) { + updateMotionStatus(formatMotionSample(sample)); + }, }; function mountApp() { @@ -19,7 +106,8 @@ function mountApp() { app = mount(App, { target, props: currentProps, - }); + }) as ReturnType & DevAppHandle; + devWindow.__tinyvectorsDeviceMotionStatus = () => app?.getDeviceMotionStatus() ?? null; } @@ -27,6 +115,9 @@ mountApp(); const themeSelect = document.getElementById('theme-select') as HTMLSelectElement; +if (themeSelect) { + themeSelect.value = currentProps.theme; +} themeSelect?.addEventListener('change', () => { currentProps.theme = themeSelect.value as 'tinyland' | 'trans' | 'pride'; mountApp(); @@ -35,6 +126,12 @@ themeSelect?.addEventListener('change', () => { const blobCountSlider = document.getElementById('blob-count') as HTMLInputElement; const blobCountValue = document.getElementById('blob-count-value'); +if (blobCountSlider) { + blobCountSlider.value = String(currentProps.blobCount); +} +if (blobCountValue) { + blobCountValue.textContent = String(currentProps.blobCount); +} blobCountSlider?.addEventListener('input', () => { currentProps.blobCount = parseInt(blobCountSlider.value, 10); if (blobCountValue) { @@ -47,19 +144,74 @@ blobCountSlider?.addEventListener('change', () => { const darkModeCheckbox = document.getElementById('dark-mode') as HTMLInputElement; +if (darkModeCheckbox) { + darkModeCheckbox.checked = initialDarkMode; +} darkModeCheckbox?.addEventListener('change', () => { document.body.classList.toggle('dark', darkModeCheckbox.checked); document.body.classList.toggle('light', !darkModeCheckbox.checked); document.documentElement.classList.toggle('dark', darkModeCheckbox.checked); + document.documentElement.classList.toggle('light', !darkModeCheckbox.checked); }); const animatedCheckbox = document.getElementById('animated') as HTMLInputElement; +if (animatedCheckbox) { + animatedCheckbox.checked = currentProps.animated; +} animatedCheckbox?.addEventListener('change', () => { currentProps.animated = animatedCheckbox.checked; mountApp(); }); +const deviceMotionCheckbox = document.getElementById('device-motion') as HTMLInputElement; +if (deviceMotionCheckbox) { + deviceMotionCheckbox.checked = currentProps.enableDeviceMotion; +} +deviceMotionCheckbox?.addEventListener('change', () => { + currentProps.enableDeviceMotion = deviceMotionCheckbox.checked; + mountApp(); +}); + +const scrollPhysicsCheckbox = document.getElementById('scroll-physics') as HTMLInputElement; +if (scrollPhysicsCheckbox) { + scrollPhysicsCheckbox.checked = currentProps.enableScrollPhysics; +} +scrollPhysicsCheckbox?.addEventListener('change', () => { + currentProps.enableScrollPhysics = scrollPhysicsCheckbox.checked; + mountApp(); +}); + +const pointerPhysicsCheckbox = document.getElementById('pointer-physics') as HTMLInputElement; +if (pointerPhysicsCheckbox) { + pointerPhysicsCheckbox.checked = currentProps.enablePointerPhysics; +} +pointerPhysicsCheckbox?.addEventListener('change', () => { + currentProps.enablePointerPhysics = pointerPhysicsCheckbox.checked; + mountApp(); +}); + +const requestMotionBtn = document.getElementById('request-motion-btn'); +requestMotionBtn?.addEventListener('click', async () => { + const granted = (await app?.requestDeviceMotionPermission()) ?? false; + updateMotionStatus(granted ? 'motion granted; waiting for sample' : 'motion unavailable'); +}); + +const calibrateMotionBtn = document.getElementById('calibrate-motion-btn'); +calibrateMotionBtn?.addEventListener('click', () => { + app?.calibrateDeviceMotion(10); + updateMotionStatus('calibration queued'); +}); + +const spoofTiltBtn = document.getElementById('spoof-tilt-btn'); +spoofTiltBtn?.addEventListener('click', () => { + spoofOrientation(120, 35, -45); +}); + +const neutralTiltBtn = document.getElementById('neutral-tilt-btn'); +neutralTiltBtn?.addEventListener('click', () => { + spoofOrientation(0, 0, 0); +}); const reloadBtn = document.getElementById('reload-btn'); reloadBtn?.addEventListener('click', () => { diff --git a/docs/physics-feel-contract.md b/docs/physics-feel-contract.md new file mode 100644 index 0000000..316ed25 --- /dev/null +++ b/docs/physics-feel-contract.md @@ -0,0 +1,61 @@ +# TinyVectors Physics Feel Contract + +TinyVectors is an expressive background system for Svelte and SvelteKit apps, not a physics demo. The animation should feel alive before any user input happens. Device motion, pointer movement, and scrolling should bias that ambient motion instead of taking control of it. + +This document is the local source of truth for the field-based interaction work tracked in Linear `TIN-853` and GitHub #40. + +## Product Intent + +- Pleasant by default: idle blobs drift, breathe, and deform subtly. +- App-safe: the component stays SSR-safe, reduced-motion aware, listener-clean, and small enough for background use. +- Stylable: gel/fluid is a visual language exposed through themes, colors, opacity, and restrained renderer controls. +- Performant: interaction work must preserve the package's bundle budget and avoid heavyweight simulation dependencies. + +## Interaction Model + +Every input should become a small field sampled by the blob physics loop: + +- Ambient field: always on, low-frequency, bounded motion. This is the baseline feel. +- Gravity field: slow directional bias from device orientation. It should make blobs lean or pool, not fall like marbles. +- Pointer field: local soft influence around the pointer. Nearby blobs should react more than distant blobs. +- Scroll field: transient impulse or stickiness that decays. It should not create permanent acceleration. +- Wall field: bounds should keep the background composed without hard visual snaps. +- Input liveness: real sensor and pointer IO should auto-enable only when available. If device-orientation events go quiet, the tab is hidden, pointer input is canceled, the pointer leaves the viewport, or the window blurs, the field must return to neutral instead of preserving stale input. + +Fields may combine, but input fields must not erase the ambient field. If a field makes the background look frozen, jittery, or overly coherent, it violates the contract. + +## Non-Goals + +- Do not revive the Phase A XSPH, soft-wall, and Gaussian anti-clustering rewrite as-is. +- Do not ship coefficient-only tuning without a contract and browser/demo validation. +- Do not introduce a heavyweight fluid solver. +- Do not make the background capture pointer events. + +## Test Strategy + +Tests should describe perceptual behavior in tolerant terms: + +- idle drift is present and bounded; +- gravity creates directional bias without overpowering all motion; +- pointer influence is local and distance-weighted; +- scroll effects decay; +- listener lifecycle stays clean; +- bundle size stays within the configured gate. + +Avoid tests that lock exact coefficients, frame-by-frame positions, or one-off screenshot pixels unless the assertion is about a real compatibility contract. + +## Current Status + +- Gravity/device-orientation is routed through `InteractionField.directionalBiasField()` and cached as a bounded force outside the per-blob hot path. +- The browser probe verifies synthetic and CDP orientation events preserve the expected motion signs, change blob geometry, and return to neutral on idle or reduced motion. +- Pointer IO currently updates the physics pointer anchor, velocity, and per-blob `mouseDistance`; it does not apply a standalone pointer force yet. +- Scroll still uses the restored pre-Phase-A path and can use the pointer anchor for sticky attraction. Route pointer and scroll through fields only after preserving the current feel and bundle headroom. + +## Implementation Slices + +1. Keep PR #39 on the restored pre-Phase-A physics and renderer baseline while retaining the motion harness, lifecycle, pointer, package, and CI work. +2. Add pure field helpers and unit tests without changing runtime feel. +3. Route gravity/device-orientation through the field helper while preserving ambient motion. +4. Route pointer and scroll values through field helpers one input at a time. +5. Add browser probes for directional bias, pointer locality, and scroll decay. +6. Revisit renderer stylability after interaction feel is stable. diff --git a/docs/release-flow.md b/docs/release-flow.md new file mode 100644 index 0000000..3c72bf3 --- /dev/null +++ b/docs/release-flow.md @@ -0,0 +1,66 @@ +# Release Flow + +This repo publishes the npm package as the primary consumer artifact. Bazel exists here to produce and validate the same package shape used by downstream Bazel consumers. + +## Authority Chain + +1. `package.json` is the npm package authority for name, version, entry points, package manager, and publish config. +2. `MODULE.bazel` mirrors the package version for Bzlmod consumers. +3. `BUILD.bazel` builds the runtime package with Vite, emits declarations with `tsc`, and assembles `//:pkg` with `npm_package`. +4. `.bazelversion` pins the Bazel runtime. Local Nix exposes `bazel` through Bazelisk so the dev shell follows that pin. +5. `.github/workflows/ci.yml` and `.github/workflows/publish.yml` call the same pinned reusable package workflow. + +`pnpm run check:release-metadata` verifies these surfaces stay aligned before CI, Bazel, or npm publish steps run. + +## Local Verification + +Run the CI checks plus the local consumer check for the Bazel-built package: + +```bash +pnpm run check:release-metadata +pnpm run check +pnpm run test +pnpm run build +pnpm run check:package +pnpm run check:bundle-size +nix develop . --command bazel build //:pkg //:package_consumer_check //:bundle_size_check //:typecheck //:test --verbose_failures +pnpm run check:package-consumer +npm pack --dry-run ./bazel-bin/pkg +npm publish --dry-run --ignore-scripts --access public ./bazel-bin/pkg +``` + +`//:package_consumer_check` and `pnpm run check:package-consumer` both validate the Bazel-built package as an installed consumer would. The pnpm command expects `./bazel-bin/pkg` to exist. It links that package into a temporary consumer workspace with the Svelte peer dependency, verifies runtime subpath exports, and runs TypeScript against the packaged declarations. + +`pnpm run check:bundle-size` measures a realistic tree-shaken consumer import, `import { TinyVectors } from '@tummycrypt/tinyvectors'`, with Svelte externalized as a peer dependency. `//:bundle_size_check` runs the same measurement against the Bazel-built package artifact. The current gate is 12 KiB gzip and the target remains 11 KiB gzip, so the check reports target headroom or overage while leaving a small CI buffer. + +The bundle-size check also reports tracked runtime modules that enter that consumer bundle. `dist/core/InteractionField.js` is expected to appear once runtime physics routes an input through the field contract, and the gzip result is the source of truth for whether that cost is acceptable. + +`bazel query //...` should also work locally. `.bazelignore` excludes direnv, Nix, package-manager, and build-output directories so Bazel does not walk generated local artifacts. + +## Compatibility Notes + +The v0.3 branch currently keeps the renderer-private `--tv-blob-intensity` custom property used by the restored three-layer renderer. Do not document a migration to `--tvi`; that abbreviation was part of the reverted gel-rendering rewrite. + +## CI Flow + +Pull requests and pushes to `main` run `Verify`, which calls `tinyland-inc/ci-templates/.github/workflows/js-bazel-package.yml` at a pinned commit. The reusable workflow: + +- installs the configured pnpm and Node major; +- runs metadata, typecheck, test, build, package, and bundle-size checks; +- builds `//:pkg //:package_consumer_check //:bundle_size_check //:typecheck //:test` through Bazelisk; +- validates the Bazel-built package with `npm pack --dry-run`; +- validates npm publication with `npm publish --dry-run --ignore-scripts`. + +This means CI treats the Bazel package output as the release candidate, not the local `dist/` directory alone. + +## Publish Flow + +Tags matching `v*` run `Publish to npm`. The publish workflow reuses the same package workflow with `dry_run: false`, downloads the Bazel-built package artifact, and publishes that isolated artifact to npm. + +The workflow has `id-token: write` because npm provenance and trusted publishing both depend on OIDC-capable CI. The current reusable template still accepts `NPM_TOKEN`; moving fully to npm trusted publishing should happen in the shared template, not only in this repo. + +## FlakeHub Status + +The flake is currently a development environment only. It does not publish TinyVectors to FlakeHub and does not expose package outputs. + +If FlakeHub publication becomes useful, add it as a separate release surface with its own workflow and metadata checks. FlakeHub publication should use its trusted-platform publishing model rather than ad hoc local publishing. diff --git a/flake.nix b/flake.nix index c0e14ea..3657547 100644 --- a/flake.nix +++ b/flake.nix @@ -4,12 +4,25 @@ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; }; - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - let pkgs = nixpkgs.legacyPackages.${system}; in { + outputs = + { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + bazel = pkgs.writeShellScriptBin "bazel" '' + exec ${pkgs.bazelisk}/bin/bazelisk "$@" + ''; + in + { devShells.default = pkgs.mkShell { buildInputs = [ - pkgs.bazel_8 + bazel + pkgs.bazelisk pkgs.nodejs_22 (pkgs.pnpm_9 or pkgs.pnpm) ]; @@ -17,10 +30,10 @@ echo "tinyvectors dev shell" echo " node $(node --version)" echo " pnpm $(pnpm --version)" - echo " bazel $(bazel --version | head -n1)" + echo " bazel $(cat .bazelversion) via bazelisk" ''; }; - formatter = pkgs.nixfmt-rfc-style; + formatter = pkgs.nixfmt; } ); } diff --git a/package.json b/package.json index 389cac7..6f679fb 100644 --- a/package.json +++ b/package.json @@ -67,19 +67,22 @@ "**/*.css" ], "scripts": { - "build": "vite build && tsc -p tsconfig.declarations.json", + "build": "vite build && node scripts/build-declarations.mjs", "dev": "vite --config vite.dev.config.ts", "dev:watch": "vite build --watch", "check": "svelte-check --tsconfig ./tsconfig.json", "check:release-metadata": "node scripts/check-release-metadata.mjs", "check:package": "publint", + "check:bundle-size": "node scripts/check-bundle-size.mjs", + "check:package-consumer": "node scripts/check-package-consumer.mjs ./bazel-bin/pkg", "test": "vitest run", + "test:browser:motion": "node scripts/probe-motion-cdp.mjs", "test:watch": "vitest", "test:pbt": "vitest run --testNamePattern='INVARIANT'", - "prepublishOnly": "pnpm run check:release-metadata && pnpm run build && pnpm run check:package" + "prepublishOnly": "pnpm run check:release-metadata && pnpm run build && pnpm run check:package && pnpm run check:bundle-size" }, "peerDependencies": { - "svelte": "^5.0.0" + "svelte": ">=5.20.0" }, "devDependencies": { "@sveltejs/package": "^2.5.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44f16ea..ef64141 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -597,8 +597,8 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.12: + resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} publint@0.2.12: @@ -1217,7 +1217,7 @@ snapshots: picomatch@4.0.4: {} - postcss@8.5.8: + postcss@8.5.12: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -1342,7 +1342,7 @@ snapshots: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.8 + postcss: 8.5.12 rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: diff --git a/scripts/build-declarations.mjs b/scripts/build-declarations.mjs new file mode 100644 index 0000000..c28a673 --- /dev/null +++ b/scripts/build-declarations.mjs @@ -0,0 +1,14 @@ +import { spawnSync } from 'node:child_process'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const tscPath = require.resolve('typescript/lib/tsc.js'); +const result = spawnSync(process.execPath, [tscPath, '-p', 'tsconfig.declarations.json'], { + stdio: 'inherit', +}); + +if (result.status !== 0) { + process.exit(result.status ?? 1); +} + +await import('./copy-svelte-declarations.mjs'); diff --git a/scripts/check-bundle-size.mjs b/scripts/check-bundle-size.mjs new file mode 100644 index 0000000..7365b0d --- /dev/null +++ b/scripts/check-bundle-size.mjs @@ -0,0 +1,119 @@ +import { existsSync } from 'node:fs'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { gzipSync } from 'node:zlib'; +import { build } from 'vite'; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const packageRoot = resolve(process.cwd(), process.argv[2] ?? '.'); +const distEntry = resolve(packageRoot, 'dist/index.js'); +const targetGzipKiB = parsePositiveKiB('TINYVECTORS_TARGET_GZIP_KIB', 11); +const maxGzipKiB = parsePositiveKiB('TINYVECTORS_MAX_GZIP_KIB', 12); +const trackedConsumerModules = ['dist/core/InteractionField.js']; + +if (maxGzipKiB < targetGzipKiB) { + console.error( + 'TINYVECTORS_MAX_GZIP_KIB must be greater than or equal to TINYVECTORS_TARGET_GZIP_KIB', + ); + process.exit(1); +} + +if (!existsSync(distEntry)) { + console.error(`Bundle entry is missing: ${distEntry}`); + console.error('Run: pnpm run build'); + process.exit(1); +} + +const tempDir = await mkdtemp(join(tmpdir(), 'tinyvectors-bundle-size-')); + +try { + const entry = join(tempDir, 'consumer-entry.js'); + await writeFile( + entry, + ` +import { TinyVectors } from ${JSON.stringify(distEntry)}; +console.log(TinyVectors); +`.trimStart(), + ); + + const output = await build({ + configFile: false, + logLevel: 'silent', + build: { + write: false, + minify: 'esbuild', + target: 'es2022', + rollupOptions: { + input: entry, + external: (id) => id === 'svelte' || id.startsWith('svelte/'), + output: { + format: 'es', + inlineDynamicImports: true, + }, + }, + }, + }); + + const outputs = Array.isArray(output) + ? output.flatMap((bundle) => bundle.output) + : output.output; + const chunks = outputs.filter((item) => item.type === 'chunk'); + const includedModules = new Set( + chunks.flatMap((chunk) => + chunk.moduleIds.map((moduleId) => moduleId.replaceAll('\\', '/')), + ), + ); + const trackedIncluded = trackedConsumerModules.filter((modulePath) => + [...includedModules].some((moduleId) => moduleId.endsWith(`/${modulePath}`)), + ); + + const js = outputs + .filter((item) => item.type === 'chunk') + .map((item) => item.code) + .join('\n'); + const rawKiB = js.length / 1024; + const gzipKiB = gzipSync(js).length / 1024; + const targetDelta = gzipKiB - targetGzipKiB; + + console.log( + [ + `bundle size check for ${relativeFromRepo(distEntry)}`, + `consumer import: { TinyVectors }`, + `raw ${rawKiB.toFixed(2)} KiB, gzip ${gzipKiB.toFixed(2)} KiB`, + `target ${targetGzipKiB.toFixed(2)} KiB, gate ${maxGzipKiB.toFixed(2)} KiB`, + targetDelta <= 0 + ? `target headroom ${Math.abs(targetDelta).toFixed(2)} KiB` + : `target overage ${targetDelta.toFixed(2)} KiB`, + trackedIncluded.length > 0 + ? `tracked modules included: ${trackedIncluded.join(', ')}` + : 'tracked modules included: none', + ].join('\n'), + ); + + if (gzipKiB > maxGzipKiB) { + console.error( + `Consumer bundle gzip ${gzipKiB.toFixed(2)} KiB exceeds ${maxGzipKiB.toFixed(2)} KiB gate`, + ); + process.exit(1); + } +} finally { + await rm(tempDir, { recursive: true, force: true }); +} + +function relativeFromRepo(path) { + return path.startsWith(`${repoRoot}/`) ? path.slice(repoRoot.length + 1) : path; +} + +function parsePositiveKiB(envName, defaultValue) { + const rawValue = process.env[envName]; + const value = rawValue == null ? defaultValue : Number(rawValue.trim()); + + if (!Number.isFinite(value) || value <= 0) { + console.error(`${envName} must be a positive number`); + process.exit(1); + } + + return value; +} diff --git a/scripts/check-package-consumer.mjs b/scripts/check-package-consumer.mjs new file mode 100644 index 0000000..b3a9784 --- /dev/null +++ b/scripts/check-package-consumer.mjs @@ -0,0 +1,163 @@ +import { spawnSync } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; + +const require = createRequire(import.meta.url); +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const packageDir = resolve(process.cwd(), process.argv[2] ?? './bazel-bin/pkg'); +const svelteDir = resolve(repoRoot, 'node_modules/svelte'); +const tscPath = require.resolve('typescript/lib/tsc.js'); + +if (!existsSync(resolve(packageDir, 'package.json'))) { + console.error(`Package directory is missing package.json: ${packageDir}`); + console.error('Run: nix develop . --command bazel build //:pkg'); + process.exit(1); +} + +if (!existsSync(resolve(svelteDir, 'package.json'))) { + console.error(`Svelte peer dependency is missing: ${svelteDir}`); + console.error('Run: pnpm install'); + process.exit(1); +} + +const tempDir = await mkdtemp(join(tmpdir(), 'tinyvectors-consumer-')); + +try { + await mkdir(join(tempDir, 'node_modules/@tummycrypt'), { recursive: true }); + await symlink( + packageDir, + join(tempDir, 'node_modules/@tummycrypt/tinyvectors'), + process.platform === 'win32' ? 'junction' : 'dir', + ); + await symlink( + svelteDir, + join(tempDir, 'node_modules/svelte'), + process.platform === 'win32' ? 'junction' : 'dir', + ); + + await writeFile( + join(tempDir, 'consumer-runtime.mjs'), + ` +import * as root from '@tummycrypt/tinyvectors'; +import * as motion from '@tummycrypt/tinyvectors/motion'; +import * as core from '@tummycrypt/tinyvectors/core'; +import * as themes from '@tummycrypt/tinyvectors/themes'; +import * as svelteComponents from '@tummycrypt/tinyvectors/svelte'; +import { readFileSync } from 'node:fs'; + +const cssUrl = import.meta.resolve('@tummycrypt/tinyvectors/themes/css'); +const css = readFileSync(new URL(cssUrl), 'utf8'); +const requiredRoot = ['BlobPhysics', 'DeviceMotion', 'TinyVectors', 'THEME_PRESETS']; +const requiredMotion = ['DeviceMotion', 'ScrollHandler', 'mapClientPointToPhysics', 'createPointerPhysicsController']; +const tinylandColors = root.THEME_PRESETS?.tinyland?.colors?.map((color) => color.color) ?? []; +const missing = [ + ...requiredRoot.filter((name) => !(name in root)).map((name) => \`root:\${name}\`), + ...requiredMotion.filter((name) => !(name in motion)).map((name) => \`motion:\${name}\`), + ...(!('BlobPhysics' in core) ? ['core:BlobPhysics'] : []), + ...(!('THEME_PRESETS' in themes) ? ['themes:THEME_PRESETS'] : []), + ...(tinylandColors.includes('rgba(139, 92, 246, 0.55)') ? [] : ['root:THEME_PRESETS.tinyland.colors']), + ...(themes.getThemePreset?.('tinyland') === themes.THEME_PRESETS?.tinyland ? [] : ['themes:getThemePreset']), + ...(!('TinyVectors' in svelteComponents) ? ['svelte:TinyVectors'] : []), + ...(!('BlobSVG' in svelteComponents) ? ['svelte:BlobSVG'] : []), + ...(css.includes('--vector-tinyland-purple') ? [] : ['themes/css:variables']), +]; + +if (missing.length > 0) { + throw new Error(\`Missing exports: \${missing.join(', ')}\`); +} +`.trimStart(), + ); + + await writeFile( + join(tempDir, 'consumer-types.ts'), + ` +import { BlobPhysics, DeviceMotion, TinyVectors, THEME_PRESETS } from '@tummycrypt/tinyvectors'; +import type { ScrollHandlerConfig as RootScrollHandlerConfig } from '@tummycrypt/tinyvectors'; +import type { ThemePreset, ThemePresetName } from '@tummycrypt/tinyvectors/core'; +import { +\tScrollHandler, +\tcreatePointerPhysicsController, +\tmapClientPointToPhysics, +\ttype MotionVector, +\ttype PointerBounds, +\ttype PointerCancelEventName, +\ttype ScrollHandlerConfig, +} from '@tummycrypt/tinyvectors/motion'; +import { getThemePreset } from '@tummycrypt/tinyvectors/themes'; +import { +\tBlobSVG, +\ttype BlobSVGProps, +\ttype TinyVectorsDeviceMotionStatus, +\ttype TinyVectorsProps, +} from '@tummycrypt/tinyvectors/svelte'; +import type { ComponentProps } from 'svelte'; + +const bounds: PointerBounds = { left: 0, top: 0, width: 100, height: 100 }; +const cancelEvent: PointerCancelEventName = 'pointercancel'; +const scrollConfig: ScrollHandlerConfig = { decayRate: 0.9, maxForces: 2 }; +const rootScrollConfig: RootScrollHandlerConfig = { maxForces: 0 }; +const scrollHandler = new ScrollHandler(scrollConfig); +const point = mapClientPointToPhysics(50, 50, bounds); +const sample: MotionVector = { x: 0, y: 0, z: 1 }; +const props: ComponentProps = { theme: 'tinyland', enableDeviceMotion: true }; +const explicitProps: TinyVectorsProps = props; +const motionStatus: TinyVectorsDeviceMotionStatus = { +\tenabled: true, +\tsupported: true, +\trequiresPermission: false, +\tpermissionState: 'granted', +\tactive: true, +}; +const blobProps: BlobSVGProps = { blobs: [] }; +const themeName: ThemePresetName = 'tinyland'; +const themePreset: ThemePreset = THEME_PRESETS[themeName]; +const names = [BlobPhysics, DeviceMotion, TinyVectors, BlobSVG, ScrollHandler, createPointerPhysicsController, THEME_PRESETS, getThemePreset, scrollHandler, scrollConfig, rootScrollConfig, point, sample, explicitProps, motionStatus, blobProps, themePreset, cancelEvent]; +console.log(names.length); +`.trimStart(), + ); + + await writeFile( + join(tempDir, 'tsconfig.json'), + `${JSON.stringify( + { + compilerOptions: { + target: 'ES2022', + module: 'NodeNext', + moduleResolution: 'NodeNext', + strict: true, + skipLibCheck: false, + noEmit: true, + }, + include: ['consumer-types.ts'], + }, + null, + 2, + )}\n`, + ); + + run(process.execPath, ['consumer-runtime.mjs'], tempDir); + run(process.execPath, [tscPath, '-p', 'tsconfig.json'], tempDir); + console.log(`package consumer check passed for ${packageDir}`); +} finally { + if (process.env.TINYVECTORS_KEEP_CONSUMER_CHECK !== '1') { + await rm(tempDir, { recursive: true, force: true }); + } else { + console.log(`kept consumer check workspace: ${tempDir}`); + } +} + +function run(command, args, cwd) { + const result = spawnSync(command, args, { + cwd, + stdio: 'inherit', + env: process.env, + }); + + if (result.status !== 0) { + process.exit(result.status ?? 1); + } +} diff --git a/scripts/check-release-metadata.mjs b/scripts/check-release-metadata.mjs index 2c95fde..0032888 100644 --- a/scripts/check-release-metadata.mjs +++ b/scripts/check-release-metadata.mjs @@ -6,7 +6,33 @@ const read = (relativePath) => const packageJson = JSON.parse(read('../package.json')); const moduleBazel = read('../MODULE.bazel'); const buildBazel = read('../BUILD.bazel'); +const ciWorkflow = read('../.github/workflows/ci.yml'); +const publishWorkflow = read('../.github/workflows/publish.yml'); const expectedPnpmVersion = packageJson.packageManager?.replace(/^pnpm@/, ''); +const expectedNodeMajor = packageJson.engines?.node?.match(/>=\s*(\d+)/)?.[1]; +const expectedBazelTargets = + '//:pkg //:package_consumer_check //:bundle_size_check //:typecheck //:test'; +const expectedPackageDir = './bazel-bin/pkg'; +const expectedPackageConsumerCommand = 'node scripts/check-package-consumer.mjs ./bazel-bin/pkg'; +const expectedPackageCheckCommand = 'pnpm run check:package && pnpm run check:bundle-size'; +const expectedPrepublishOnlyCommand = + 'pnpm run check:release-metadata && pnpm run build && pnpm run check:package && pnpm run check:bundle-size'; +const expectedSharedWorkflowInputs = { + runner_mode: 'hosted', + workspace_mode: 'isolated', + publish_mode: 'same_runner', + node_versions: `["${expectedNodeMajor}"]`, + publish_node_version: expectedNodeMajor, + pnpm_version: expectedPnpmVersion, + metadata_check_command: 'pnpm run check:release-metadata', + typecheck_command: 'pnpm run check', + unit_test_command: 'pnpm run test', + build_command: 'pnpm run build', + package_check_command: expectedPackageCheckCommand, + bazel_targets: expectedBazelTargets, + package_dir: expectedPackageDir, + npm_access: 'public', +}; const extract = (source, pattern, label) => { const match = source.match(pattern); @@ -16,6 +42,24 @@ const extract = (source, pattern, label) => { return match[1]; }; +const extractWorkflowValue = (source, key, label) => { + const rawValue = extract(source, new RegExp(`^\\s*${key}:\\s*(.+?)\\s*$`, 'm'), label).trim(); + if ( + (rawValue.startsWith("'") && rawValue.endsWith("'")) || + (rawValue.startsWith('"') && rawValue.endsWith('"')) + ) { + return rawValue.slice(1, -1); + } + return rawValue; +}; + +const sharedPackageWorkflow = (source, label) => + extract( + source, + /uses:\s*(tinyland-inc\/ci-templates\/\.github\/workflows\/js-bazel-package\.yml@[0-9a-f]{40})/, + label, + ); + const checks = [ { label: 'MODULE.bazel version', @@ -49,10 +93,68 @@ const checks = [ actual: extract(moduleBazel, /pnpm_version = "([^"]+)"/, 'pnpm_version'), expected: expectedPnpmVersion, }, + { + label: 'MODULE.bazel Node toolchain major', + actual: extract(moduleBazel, /node_version = "(\d+)\./, 'node_version'), + expected: expectedNodeMajor, + }, + { + label: 'CI reusable package workflow', + actual: sharedPackageWorkflow(ciWorkflow, 'CI reusable workflow'), + expected: sharedPackageWorkflow(publishWorkflow, 'publish reusable workflow'), + }, + { + label: 'package consumer check script', + actual: packageJson.scripts?.['check:package-consumer'], + expected: expectedPackageConsumerCommand, + }, + { + label: 'bundle size check script', + actual: packageJson.scripts?.['check:bundle-size'], + expected: 'node scripts/check-bundle-size.mjs', + }, + { + label: 'prepublishOnly script', + actual: packageJson.scripts?.prepublishOnly, + expected: expectedPrepublishOnlyCommand, + }, + { + label: 'CI publish dry-run', + actual: extractWorkflowValue(ciWorkflow, 'dry_run', 'CI dry_run'), + expected: 'true', + }, + { + label: 'tag publish dry-run', + actual: extractWorkflowValue(publishWorkflow, 'dry_run', 'publish dry_run'), + expected: 'false', + }, ]; +for (const [key, expected] of Object.entries(expectedSharedWorkflowInputs)) { + checks.push( + { + label: `CI ${key}`, + actual: extractWorkflowValue(ciWorkflow, key, `CI ${key}`), + expected, + }, + { + label: `publish ${key}`, + actual: extractWorkflowValue(publishWorkflow, key, `publish ${key}`), + expected, + }, + ); +} + const failures = checks.filter((check) => check.actual !== check.expected); +if (packageJson.publishConfig?.provenance && !/id-token:\s*write/.test(publishWorkflow)) { + failures.push({ + label: 'publish workflow id-token permission', + actual: 'missing', + expected: 'write', + }); +} + if (failures.length > 0) { for (const failure of failures) { console.error( diff --git a/scripts/copy-svelte-declarations.mjs b/scripts/copy-svelte-declarations.mjs new file mode 100644 index 0000000..c14489e --- /dev/null +++ b/scripts/copy-svelte-declarations.mjs @@ -0,0 +1,32 @@ +import { copyFile, mkdir, rm, writeFile } from 'node:fs/promises'; + +const declarationPairs = [ + ['BlobSVG.svelte.d.ts', 'BlobSVG.d.ts'], + ['TinyVectors.svelte.d.ts', 'TinyVectors.d.ts'], +]; +const sourceDir = new URL('../src/svelte/', import.meta.url); +const outputDir = new URL('../dist-types/svelte/', import.meta.url); + +await mkdir(outputDir, { recursive: true }); + +await Promise.all( + declarationPairs.map(([sourceName, outputName]) => + copyFile(new URL(sourceName, sourceDir), new URL(outputName, outputDir)), + ), +); + +await writeFile( + new URL('index.d.ts', outputDir), + [ + "export { default as TinyVectors, type TinyVectorsExports, type TinyVectorsProps } from './TinyVectors.js';", + "export type { TinyVectorsDeviceMotionStatus } from './types.js';", + "export { default as BlobSVG, type BlobSVGProps } from './BlobSVG.js';", + '', + ].join('\n'), +); + +await Promise.all([ + rm(new URL('index.d.ts.map', outputDir), { force: true }), + rm(new URL('BlobSVG.svelte.d.ts', outputDir), { force: true }), + rm(new URL('TinyVectors.svelte.d.ts', outputDir), { force: true }), +]); diff --git a/scripts/probe-motion-cdp.mjs b/scripts/probe-motion-cdp.mjs new file mode 100644 index 0000000..4aaac0e --- /dev/null +++ b/scripts/probe-motion-cdp.mjs @@ -0,0 +1,676 @@ +import { spawn, spawnSync } from 'node:child_process'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +const host = '127.0.0.1'; +const vitePort = Number(process.env.TINYVECTORS_VITE_PORT ?? 5176); +const viteHmrPort = Number(process.env.TINYVECTORS_VITE_HMR_PORT ?? vitePort + 19000); +const cdpPort = Number(process.env.TINYVECTORS_CDP_PORT ?? 9228); +const chromePath = findChrome(); + +if (!chromePath) { + console.error('Chrome executable not found. Set CHROME_PATH to run this probe.'); + process.exit(1); +} + +const children = new Set(); + +process.on('exit', () => { + for (const child of children) { + child.kill('SIGTERM'); + } +}); + +function spawnChild(command, args, options = {}) { + const child = spawn(command, args, { + stdio: ['ignore', 'pipe', 'pipe'], + env: process.env, + ...options, + }); + children.add(child); + child.once('exit', () => children.delete(child)); + return child; +} + +function findChrome() { + if (process.env.CHROME_PATH) return process.env.CHROME_PATH; + + const candidates = [ + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', + 'google-chrome', + 'google-chrome-stable', + 'chromium', + 'chromium-browser', + ]; + + for (const candidate of candidates) { + if (candidate.startsWith('/')) { + const result = spawnSync('test', ['-x', candidate]); + if (result.status === 0) return candidate; + continue; + } + + const result = spawnSync('command', ['-v', candidate], { + shell: true, + encoding: 'utf8', + }); + if (result.status === 0 && result.stdout.trim()) { + return result.stdout.trim(); + } + } + + return null; +} + +async function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function waitForFetch(url, timeoutMs = 15000) { + const started = Date.now(); + let lastError; + + while (Date.now() - started < timeoutMs) { + try { + const response = await fetch(url); + if (response.ok) return response; + lastError = new Error(`${response.status} ${response.statusText}`); + } catch (error) { + lastError = error; + } + await delay(100); + } + + throw lastError ?? new Error(`Timed out waiting for ${url}`); +} + +async function waitForJson(url, timeoutMs = 15000) { + const response = await waitForFetch(url, timeoutMs); + return await response.json(); +} + +async function terminateChildren() { + const exiting = [...children].map( + (child) => + new Promise((resolve) => { + child.once('exit', resolve); + child.kill('SIGTERM'); + setTimeout(resolve, 2000); + }), + ); + + await Promise.all(exiting); + children.clear(); +} + +async function removeDirectoryWithRetry(directory) { + for (let attempt = 0; attempt < 5; attempt++) { + try { + await rm(directory, { recursive: true, force: true }); + return; + } catch (error) { + if (attempt === 4) throw error; + await delay(200); + } + } +} + +class CdpClient { + constructor(url) { + this.nextId = 1; + this.pending = new Map(); + this.ws = new WebSocket(url); + this.ready = new Promise((resolve, reject) => { + this.ws.addEventListener('open', resolve, { once: true }); + this.ws.addEventListener('error', reject, { once: true }); + }); + this.ws.addEventListener('message', (event) => { + const message = JSON.parse(event.data); + if (!message.id || !this.pending.has(message.id)) return; + + const pending = this.pending.get(message.id); + this.pending.delete(message.id); + + if (message.error) { + pending.reject(new Error(`${message.error.code}: ${message.error.message}`)); + } else { + pending.resolve(message.result ?? {}); + } + }); + } + + async send(method, params = {}) { + await this.ready; + + const id = this.nextId++; + this.ws.send(JSON.stringify({ id, method, params })); + + return await new Promise((resolve, reject) => { + const timer = setTimeout(() => { + if (!this.pending.has(id)) return; + this.pending.delete(id); + reject(new Error(`Timed out waiting for ${method}`)); + }, 10000); + + this.pending.set(id, { + resolve(value) { + clearTimeout(timer); + resolve(value); + }, + reject(error) { + clearTimeout(timer); + reject(error); + }, + }); + }); + } + + close() { + this.ws.close(); + } +} + +async function evaluate(client, expression) { + const result = await client.send('Runtime.evaluate', { + expression, + awaitPromise: true, + returnByValue: true, + }); + + if (result.exceptionDetails) { + throw new Error(JSON.stringify(result.exceptionDetails)); + } + + return result.result.value; +} + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function parseMotionStatus(status) { + const match = /^motion x (-?\d+(?:\.\d+)?) y (-?\d+(?:\.\d+)?) z (-?\d+(?:\.\d+)?)$/.exec(status ?? ''); + if (!match) return null; + return { + x: Number(match[1]), + y: Number(match[2]), + z: Number(match[3]), + }; +} + +let chromeProfile; +let client; + +try { + const vite = spawnChild('pnpm', [ + 'exec', + 'vite', + '--config', + 'vite.dev.config.ts', + '--host', + host, + '--port', + String(vitePort), + ], { + env: { + ...process.env, + CI: 'true', + TINYVECTORS_VITE_PORT: String(vitePort), + TINYVECTORS_VITE_HMR_PORT: String(viteHmrPort), + }, + }); + + vite.stderr.on('data', (chunk) => { + process.stderr.write(chunk); + }); + + await waitForFetch(`http://${host}:${vitePort}/`); + + chromeProfile = await mkdtemp(join(tmpdir(), 'tinyvectors-cdp-profile-')); + const chrome = spawnChild(chromePath, [ + '--headless=new', + '--disable-gpu', + '--disable-dev-shm-usage', + '--no-first-run', + '--no-default-browser-check', + `--remote-debugging-address=${host}`, + `--remote-debugging-port=${cdpPort}`, + `--user-data-dir=${chromeProfile}`, + 'about:blank', + ]); + + chrome.stderr.on('data', (chunk) => { + if (process.env.DEBUG_CHROME === 'true') { + process.stderr.write(chunk); + } + }); + + const version = await waitForJson(`http://${host}:${cdpPort}/json/version`); + const tabs = await waitForJson(`http://${host}:${cdpPort}/json/list`); + const page = tabs.find((tab) => tab.type === 'page') ?? tabs[0]; + + client = new CdpClient(page.webSocketDebuggerUrl); + await client.send('Runtime.enable'); + await client.send('Page.enable'); + await client.send('Page.addScriptToEvaluateOnNewDocument', { + source: ` + window.__tinyvectorsEvents = []; + (() => { + const originalAddEventListener = EventTarget.prototype.addEventListener; + const originalRemoveEventListener = EventTarget.prototype.removeEventListener; + const listenerIds = new WeakMap(); + const activeWindowListeners = new Map(); + let nextListenerId = 1; + + const listenerId = (listener) => { + if ((typeof listener !== 'function' && typeof listener !== 'object') || listener === null) { + return String(listener); + } + if (!listenerIds.has(listener)) { + listenerIds.set(listener, nextListenerId++); + } + return listenerIds.get(listener); + }; + + window.__tinyvectorsListenerLedger = { + snapshot() { + const counts = {}; + for (const type of activeWindowListeners.values()) { + counts[type] = (counts[type] || 0) + 1; + } + return counts; + }, + }; + + EventTarget.prototype.addEventListener = function(type, listener, options) { + if (this === window && listener) { + activeWindowListeners.set(type + ':' + listenerId(listener), type); + } + return originalAddEventListener.call(this, type, listener, options); + }; + + EventTarget.prototype.removeEventListener = function(type, listener, options) { + if (this === window && listener) { + activeWindowListeners.delete(type + ':' + listenerId(listener)); + } + return originalRemoveEventListener.call(this, type, listener, options); + }; + + originalAddEventListener.call(window, 'deviceorientation', (event) => { + window.__tinyvectorsEvents.push({ + type: 'deviceorientation', + alpha: event.alpha, + beta: event.beta, + gamma: event.gamma, + at: performance.now() + }); + }); + originalAddEventListener.call(window, 'devicemotion', (event) => { + const gravity = event.accelerationIncludingGravity; + window.__tinyvectorsEvents.push({ + type: 'devicemotion', + x: gravity && gravity.x, + y: gravity && gravity.y, + z: gravity && gravity.z, + at: performance.now() + }); + }); + })(); + `, + }); + + const disabledUrl = `http://${host}:${vitePort}/?controls=true&animated=true&deviceMotion=false&pointerPhysics=false&scrollPhysics=false&blobs=8&listenerProbe=1`; + await client.send('Page.navigate', { url: disabledUrl }); + await delay(1000); + + const disabledInitial = await evaluate(client, `({ + motionStatus: window.__tinyvectorsDeviceMotionStatus?.() ?? null, + pathCount: document.querySelectorAll('path').length, + listeners: window.__tinyvectorsListenerLedger?.snapshot?.() ?? {} + })`); + + assert(disabledInitial.pathCount > 0, 'TinyVectors did not render when IO features were disabled.'); + assert( + disabledInitial.motionStatus?.enabled === false, + 'Disabled device motion page reported device motion enabled.', + ); + assert( + disabledInitial.motionStatus?.active === false, + 'Disabled device motion page reported an active listener.', + ); + assert(!disabledInitial.listeners.wheel, 'Wheel listener attached when scroll physics was disabled.'); + assert( + !disabledInitial.listeners.pointermove, + 'Pointer listener attached when pointer physics was disabled.', + ); + assert( + !disabledInitial.listeners.pointercancel, + 'Pointer cancel listener attached when pointer physics was disabled.', + ); + assert( + !disabledInitial.listeners.deviceorientation, + 'Device orientation listener attached when device motion was disabled.', + ); + + const pageUrl = `http://${host}:${vitePort}/?controls=true&animated=true&deviceMotion=true&pointerPhysics=false&scrollPhysics=false&blobs=8&motionIdleReset=700`; + await client.send('Page.navigate', { url: pageUrl }); + await delay(1500); + + const initial = await evaluate(client, `({ + secure: window.isSecureContext, + hasDeviceMotionEvent: 'DeviceMotionEvent' in window, + hasDeviceOrientationEvent: 'DeviceOrientationEvent' in window, + hasAccelerometer: 'Accelerometer' in window, + motionStatus: window.__tinyvectorsDeviceMotionStatus?.() ?? null, + status: document.getElementById('motion-status')?.textContent ?? null, + pathCount: document.querySelectorAll('path').length, + firstPath: document.querySelector('path')?.getAttribute('d') ?? null, + events: window.__tinyvectorsEvents + })`); + + assert(initial.secure, 'Page must be a secure context for device motion APIs.'); + assert(initial.hasDeviceOrientationEvent, 'DeviceOrientationEvent is not exposed in Chrome.'); + assert(initial.pathCount > 0, 'TinyVectors SVG paths were not rendered.'); + assert(initial.motionStatus?.enabled === true, 'Device motion status did not report enabled.'); + assert(initial.motionStatus?.supported === true, 'Device motion status did not report support.'); + assert( + initial.motionStatus?.permissionState === 'granted', + `Device motion status did not report granted; got ${initial.motionStatus?.permissionState}.`, + ); + assert(initial.motionStatus?.active === true, 'Device motion status did not report active listener.'); + + await client.send('Runtime.evaluate', { + expression: `document.getElementById('spoof-tilt-btn')?.click()`, + awaitPromise: true, + }); + await delay(350); + + const afterSpoof = await evaluate(client, `({ + status: document.getElementById('motion-status')?.textContent ?? null, + firstPath: document.querySelector('path')?.getAttribute('d') ?? null, + events: window.__tinyvectorsEvents + })`); + + assert( + afterSpoof.status?.startsWith('motion x '), + `Synthetic orientation did not reach TinyVectors; status was ${afterSpoof.status}`, + ); + assert(afterSpoof.events.length > initial.events.length, 'Synthetic orientation was not observed.'); + assert(afterSpoof.firstPath !== initial.firstPath, 'Synthetic orientation did not change blob geometry.'); + const syntheticMotion = parseMotionStatus(afterSpoof.status); + assert(syntheticMotion, `Synthetic orientation status was not parseable: ${afterSpoof.status}`); + assert( + syntheticMotion.x < 0 && syntheticMotion.y > 0, + `Synthetic orientation did not preserve expected direction; got ${afterSpoof.status}`, + ); + + await delay(550); + const afterIdleReset = await evaluate(client, `({ + status: document.getElementById('motion-status')?.textContent ?? null, + events: window.__tinyvectorsEvents + })`); + + assert( + afterIdleReset.status === 'motion x 0.00 y 0.00 z 0.00', + `Device orientation idle reset did not neutralize motion; status was ${afterIdleReset.status}`, + ); + + await client.send('Emulation.setEmulatedMedia', { + features: [{ name: 'prefers-reduced-motion', value: 'reduce' }], + }); + await delay(350); + + const afterReducedMotion = await evaluate(client, `({ + motionStatus: window.__tinyvectorsDeviceMotionStatus?.() ?? null, + status: document.getElementById('motion-status')?.textContent ?? null, + listeners: window.__tinyvectorsListenerLedger?.snapshot?.() ?? {} + })`); + + assert( + afterReducedMotion.status === 'motion x 0.00 y 0.00 z 0.00', + `Reduced motion did not neutralize active motion; status was ${afterReducedMotion.status}`, + ); + assert( + afterReducedMotion.motionStatus?.active === false, + 'Reduced motion did not stop the device orientation listener.', + ); + assert( + !afterReducedMotion.listeners.deviceorientation, + 'Device orientation listener leaked while reduced motion was enabled.', + ); + + await client.send('Emulation.setEmulatedMedia', { + features: [{ name: 'prefers-reduced-motion', value: 'no-preference' }], + }); + await delay(350); + + const afterReducedMotionRestore = await evaluate(client, `({ + motionStatus: window.__tinyvectorsDeviceMotionStatus?.() ?? null, + listeners: window.__tinyvectorsListenerLedger?.snapshot?.() ?? {} + })`); + + assert( + afterReducedMotionRestore.motionStatus?.active === true, + 'Device orientation listener did not restart after reduced motion was disabled.', + ); + assert( + afterReducedMotionRestore.listeners.deviceorientation === 1, + `Expected one deviceorientation listener after reduced motion restore, got ${afterReducedMotionRestore.listeners.deviceorientation}.`, + ); + + await client.send('DeviceOrientation.setDeviceOrientationOverride', { + alpha: 180, + beta: 50, + gamma: -40, + }); + await delay(350); + + const afterCdpOrientation = await evaluate(client, `({ + status: document.getElementById('motion-status')?.textContent ?? null, + firstPath: document.querySelector('path')?.getAttribute('d') ?? null, + events: window.__tinyvectorsEvents + })`); + + assert( + afterCdpOrientation.events.length > afterSpoof.events.length, + 'CDP device orientation override did not emit a page event.', + ); + assert( + afterCdpOrientation.firstPath !== afterSpoof.firstPath, + 'CDP device orientation override did not change blob geometry.', + ); + const cdpOrientationMotion = parseMotionStatus(afterCdpOrientation.status); + assert( + cdpOrientationMotion, + `CDP orientation status was not parseable: ${afterCdpOrientation.status}`, + ); + assert( + cdpOrientationMotion.x < 0 && cdpOrientationMotion.y > 0, + `CDP orientation did not preserve expected direction; got ${afterCdpOrientation.status}`, + ); + + const listenerProbeUrl = `http://${host}:${vitePort}/?controls=true&animated=true&deviceMotion=true&pointerPhysics=true&scrollPhysics=true&blobs=8&listenerProbe=1`; + await client.send('Page.navigate', { url: listenerProbeUrl }); + await delay(1500); + + const listenerInitial = await evaluate(client, `({ + pathCount: document.querySelectorAll('path').length, + bodyPathCount: document.querySelectorAll('svg g')[1]?.querySelectorAll('path').length ?? 0, + gradientCount: document.querySelectorAll('radialGradient').length, + listeners: window.__tinyvectorsListenerLedger?.snapshot?.() ?? {} + })`); + + assert(listenerInitial.pathCount === 24, `Expected 24 SVG paths, got ${listenerInitial.pathCount}.`); + assert(listenerInitial.bodyPathCount === 8, `Expected 8 body paths, got ${listenerInitial.bodyPathCount}.`); + assert( + listenerInitial.gradientCount === 24, + `Expected 24 radial gradients, got ${listenerInitial.gradientCount}.`, + ); + assert( + listenerInitial.listeners.wheel === 1, + `Expected one wheel listener, got ${listenerInitial.listeners.wheel}.`, + ); + assert( + listenerInitial.listeners.pointermove === 1, + `Expected one pointermove listener, got ${listenerInitial.listeners.pointermove}.`, + ); + assert( + listenerInitial.listeners.pointerout === 1, + `Expected one pointerout listener, got ${listenerInitial.listeners.pointerout}.`, + ); + assert( + listenerInitial.listeners.pointercancel === 1, + `Expected one pointercancel listener, got ${listenerInitial.listeners.pointercancel}.`, + ); + assert( + listenerInitial.listeners.blur === 1, + `Expected one blur listener, got ${listenerInitial.listeners.blur}.`, + ); + assert( + listenerInitial.listeners.deviceorientation === 1, + `Expected one deviceorientation listener, got ${listenerInitial.listeners.deviceorientation}.`, + ); + + await client.send('Runtime.evaluate', { + expression: `document.getElementById('scroll-physics')?.click()`, + awaitPromise: true, + }); + await delay(300); + const afterScrollOff = await evaluate(client, `({ + listeners: window.__tinyvectorsListenerLedger?.snapshot?.() ?? {} + })`); + assert(!afterScrollOff.listeners.wheel, 'Wheel listener leaked after disabling scroll physics.'); + + await client.send('Runtime.evaluate', { + expression: `document.getElementById('pointer-physics')?.click()`, + awaitPromise: true, + }); + await delay(300); + const afterPointerOff = await evaluate(client, `({ + listeners: window.__tinyvectorsListenerLedger?.snapshot?.() ?? {} + })`); + assert(!afterPointerOff.listeners.pointermove, 'Pointer listener leaked after disabling pointer physics.'); + assert(!afterPointerOff.listeners.pointerout, 'Pointer exit listener leaked after disabling pointer physics.'); + assert(!afterPointerOff.listeners.pointercancel, 'Pointer cancel listener leaked after disabling pointer physics.'); + assert(!afterPointerOff.listeners.blur, 'Pointer blur listener leaked after disabling pointer physics.'); + + await client.send('Runtime.evaluate', { + expression: `document.getElementById('device-motion')?.click()`, + awaitPromise: true, + }); + await delay(300); + const afterDeviceMotionOff = await evaluate(client, `({ + listeners: window.__tinyvectorsListenerLedger?.snapshot?.() ?? {} + })`); + assert( + !afterDeviceMotionOff.listeners.deviceorientation, + 'Device orientation listener leaked after disabling device motion.', + ); + + await client.send('Emulation.setSensorOverrideEnabled', { + type: 'accelerometer', + enabled: true, + metadata: { available: true, minimumFrequency: 1, maximumFrequency: 60 }, + }); + await client.send('Page.navigate', { url: `${pageUrl}&accelerometerProbe=1` }); + await delay(1500); + + const beforeCdpAccelerometer = await evaluate(client, `({ + hasAccelerometer: 'Accelerometer' in window, + status: document.getElementById('motion-status')?.textContent ?? null, + firstPath: document.querySelector('path')?.getAttribute('d') ?? null, + events: window.__tinyvectorsEvents + })`); + + await client.send('Emulation.setSensorOverrideReadings', { + type: 'accelerometer', + reading: { + xyz: { + x: 4, + y: -3, + z: 9.80665, + }, + }, + }); + await delay(1000); + + const afterCdpAccelerometer = await evaluate(client, `({ + status: document.getElementById('motion-status')?.textContent ?? null, + firstPath: document.querySelector('path')?.getAttribute('d') ?? null, + events: window.__tinyvectorsEvents + })`); + + const cdpAccelerometerChanged = + afterCdpAccelerometer.firstPath !== beforeCdpAccelerometer.firstPath; + + console.log( + JSON.stringify( + { + chrome: version.Browser, + pageUrl, + initial: { + secure: initial.secure, + hasDeviceMotionEvent: initial.hasDeviceMotionEvent, + hasDeviceOrientationEvent: initial.hasDeviceOrientationEvent, + hasAccelerometer: initial.hasAccelerometer, + motionStatus: initial.motionStatus, + pathCount: initial.pathCount, + }, + disabledInitial: { + motionStatus: disabledInitial.motionStatus, + pathCount: disabledInitial.pathCount, + listeners: disabledInitial.listeners, + }, + syntheticOrientation: { + status: afterSpoof.status, + motion: syntheticMotion, + events: afterSpoof.events.length, + pathChanged: afterSpoof.firstPath !== initial.firstPath, + idleResetStatus: afterIdleReset.status, + }, + reducedMotion: { + status: afterReducedMotion.status, + activeAfterReduce: afterReducedMotion.motionStatus?.active, + listenersAfterReduce: afterReducedMotion.listeners, + activeAfterRestore: afterReducedMotionRestore.motionStatus?.active, + listenersAfterRestore: afterReducedMotionRestore.listeners, + }, + cdpOrientation: { + status: afterCdpOrientation.status, + motion: cdpOrientationMotion, + events: afterCdpOrientation.events.length, + pathChanged: afterCdpOrientation.firstPath !== afterSpoof.firstPath, + lastEvent: afterCdpOrientation.events.at(-1), + }, + cdpAccelerometer: { + hasAccelerometer: beforeCdpAccelerometer.hasAccelerometer, + status: afterCdpAccelerometer.status, + windowEvents: afterCdpAccelerometer.events.length, + pathChanged: cdpAccelerometerChanged, + note: 'TinyVectors uses DeviceOrientationEvent/TiltSource; raw accelerometer CDP is informational.', + }, + listenerLifecycle: { + initial: listenerInitial.listeners, + afterScrollOff: afterScrollOff.listeners, + afterPointerOff: afterPointerOff.listeners, + afterDeviceMotionOff: afterDeviceMotionOff.listeners, + }, + }, + null, + 2, + ), + ); +} catch (error) { + console.error(error); + process.exitCode = 1; +} finally { + client?.close(); + await terminateChildren(); + if (chromeProfile) { + await removeDirectoryWithRetry(chromeProfile); + } +} diff --git a/src/core/BlobPhysics.ts b/src/core/BlobPhysics.ts index 5bb61bc..1f22e88 100644 --- a/src/core/BlobPhysics.ts +++ b/src/core/BlobPhysics.ts @@ -16,6 +16,10 @@ import type { ConvexBlob, GravityVector, TiltVector } from './types.js'; import { SpatialHash } from './SpatialHash.js'; import { GaussianKernel } from './GaussianKernel.js'; import { SpringSystem, DEFAULT_SPRING_CONFIG, type SpringConfig } from './SpringSystem.js'; +import { directionalBiasField } from './InteractionField.js'; + +const ACCELEROMETER_STRENGTH = 0.0008; +const ACCELEROMETER_MAX_FORCE = 0.003; export interface BlobPhysicsConfig { antiClusteringStrength: number; @@ -33,7 +37,7 @@ export interface BlobPhysicsConfig { springConfig: Partial; } -const DEFAULT_CONFIG: BlobPhysicsConfig = { +export const DEFAULT_BLOB_PHYSICS_CONFIG: BlobPhysicsConfig = { antiClusteringStrength: 0.15, bounceDamping: 0.7, deformationSpeed: 0.5, @@ -61,6 +65,7 @@ export class BlobPhysics { private gravity: GravityVector = { x: 0, y: 0 }; + private gravityField: GravityVector = { x: 0, y: 0 }; private tilt: TiltVector = { x: 0, y: 0, z: 0 }; private scrollStickiness = 0; @@ -72,15 +77,11 @@ export class BlobPhysics { private spatialHash: SpatialHash; private gaussianKernel: GaussianKernel; private springSystem: SpringSystem; - - // Pre-allocated scratch buffers for hot-path passes (no per-frame allocation). - private skinTensionScratch: Float32Array | null = null; - private xsphDvX: Float32Array | null = null; - private xsphDvY: Float32Array | null = null; + private controlRadiusScratch: number[] = []; constructor(numBlobs: number, config: Partial = {}) { this.numBlobs = numBlobs; - this.config = { ...DEFAULT_CONFIG, ...config }; + this.config = { ...DEFAULT_BLOB_PHYSICS_CONFIG, ...config }; this.spatialHash = new SpatialHash(60); @@ -120,6 +121,11 @@ export class BlobPhysics { setGravity(gravity: GravityVector): void { this.gravity = gravity; + this.gravityField = directionalBiasField( + gravity, + ACCELEROMETER_STRENGTH, + ACCELEROMETER_MAX_FORCE, + ); } @@ -159,70 +165,16 @@ export class BlobPhysics { this.updateScreensaverPhysics(blob, deltaTime, time) ); - // XSPH viscosity coupling — each blob's velocity drifts toward its - // neighborhood-weighted velocity. This is what makes the swarm - // behave as a fluid rather than 5 independent things; drag bleeds - // absolute motion, XSPH bleeds *relative* motion between neighbors. - // Macklin & Müller, Position Based Fluids, SIGGRAPH 2013. - this.applyXSPHCoupling(); - - + this.mouseVelX *= 0.96; this.mouseVelY *= 0.96; } - private applyXSPHCoupling(): void { - const blobs = this.blobs; - const n = blobs.length; - if (n < 2) return; - - if (!this.xsphDvX || !this.xsphDvY || this.xsphDvX.length < n) { - this.xsphDvX = new Float32Array(n); - this.xsphDvY = new Float32Array(n); - } - const dvX = this.xsphDvX; - const dvY = this.xsphDvY; - dvX.fill(0); - dvY.fill(0); - - const eps = 0.4; - const sigma = 80; - const twoSigmaSq = 2 * sigma * sigma; - - for (let i = 0; i < n; i++) { - const a = blobs[i]; - for (let j = i + 1; j < n; j++) { - const b = blobs[j]; - const dx = b.currentX - a.currentX; - const dy = b.currentY - a.currentY; - const w = Math.exp(-(dx * dx + dy * dy) / twoSigmaSq); - const dvx = w * (b.velocityX - a.velocityX); - const dvy = w * (b.velocityY - a.velocityY); - dvX[i] += dvx; - dvY[i] += dvy; - dvX[j] -= dvx; - dvY[j] -= dvy; - } - } - - for (let i = 0; i < n; i++) { - blobs[i].velocityX += eps * dvX[i]; - blobs[i].velocityY += eps * dvY[i]; - } - } - - // Anti-clustering with Gaussian-falloff repulsion. The previous step- - // function variant ((distance < requiredDistance) ? force : 0, plus - // a separate sharp proximity multiplier at requiredDistance * 0.7) - // produced a discontinuous force read as a "click" on near-contact. - // exp(-r² / 2σ²) is C∞ smooth — force grows continuously, peaks at - // zero distance, decays smoothly. Reuses the same Gaussian family as - // the existing GaussianKernel. private applyAntiClusteringWithSpatialHash(): void { - const maxPersonalSpace = 60; + const maxPersonalSpace = 60; for (const blob of this.blobs) { const neighbors = this.spatialHash.queryNeighbors(blob, maxPersonalSpace); @@ -231,45 +183,39 @@ export class BlobPhysics { const dx = other.currentX - blob.currentX; const dy = other.currentY - blob.currentY; const distance = Math.sqrt(dx * dx + dy * dy); - if (distance <= 0) continue; - const requiredDistance = Math.max( - blob.personalSpace || 50, - other.personalSpace || 50 - ); - const sigma = requiredDistance * 0.5; - const w = Math.exp(-(distance * distance) / (2 * sigma * sigma)); - const repulsionForce = - w * 0.055 * (this.config.antiClusteringStrength / 0.15); - - const normalizedDx = dx / distance; - const normalizedDy = dy / distance; - const forceMultiplier = blob.repulsionStrength || 0.03; - - blob.velocityX -= normalizedDx * repulsionForce * forceMultiplier; - blob.velocityY -= normalizedDy * repulsionForce * forceMultiplier; - - // Force is now Gaussian (continuous, applies at any range - // inside the spatial-hash query). lastRepulsionTime stays - // gated on the close-contact threshold because downstream - // addEscapeVelocity uses it as a "blobs were just pushing - // each other apart" event detector — not as a generic - // "any neighbor contributed" flag. Decoupling is intentional. - if (distance < requiredDistance) { + const requiredDistance = Math.max(blob.personalSpace || 50, other.personalSpace || 50); + + if (distance < requiredDistance && distance > 0) { + const overlap = requiredDistance - distance; + const repulsionForce = (overlap / requiredDistance) * 0.055 * this.config.antiClusteringStrength / 0.15; + + const normalizedDx = dx / distance; + const normalizedDy = dy / distance; + + const forceMultiplier = blob.repulsionStrength || 0.03; + const proximityMultiplier = distance < requiredDistance * 0.7 ? 3.5 : 1.0; + + + blob.velocityX -= normalizedDx * repulsionForce * forceMultiplier * proximityMultiplier * 0.5; + blob.velocityY -= normalizedDy * repulsionForce * forceMultiplier * proximityMultiplier * 0.5; + blob.lastRepulsionTime = Date.now(); } } } } - + updateMousePosition(x: number, y: number): void { - this.mouseVelX = x - this.lastMouseX; - this.mouseVelY = y - this.lastMouseY; - this.lastMouseX = this.mouseX; - this.lastMouseY = this.mouseY; + const previousMouseX = this.mouseX; + const previousMouseY = this.mouseY; + this.mouseVelX = x - previousMouseX; + this.mouseVelY = y - previousMouseY; + this.lastMouseX = previousMouseX; + this.lastMouseY = previousMouseY; this.mouseX = x; this.mouseY = y; } @@ -460,9 +406,6 @@ export class BlobPhysics { } } - // Fallback when useSpatialHash is false. Same Gaussian-falloff - // repulsion as applyAntiClusteringWithSpatialHash, applied - // pairwise in O(N²). private applyEnhancedAntiClustering(): void { for (let i = 0; i < this.blobs.length; i++) { const blob1 = this.blobs[i]; @@ -473,29 +416,28 @@ export class BlobPhysics { const dx = blob2.currentX - blob1.currentX; const dy = blob2.currentY - blob1.currentY; const distance = Math.sqrt(dx * dx + dy * dy); - if (distance <= 0) continue; - const requiredDistance = Math.max( - blob1.personalSpace || 50, - blob2.personalSpace || 50 - ); - const sigma = requiredDistance * 0.5; - const w = Math.exp(-(distance * distance) / (2 * sigma * sigma)); - const repulsionForce = - w * 0.055 * (this.config.antiClusteringStrength / 0.15); + const requiredDistance = Math.max(blob1.personalSpace || 50, blob2.personalSpace || 50); - const normalizedDx = dx / distance; - const normalizedDy = dy / distance; - const force1Multiplier = blob1.repulsionStrength || 0.03; - const force2Multiplier = blob2.repulsionStrength || 0.03; + if (distance < requiredDistance && distance > 0) { + const overlap = requiredDistance - distance; + const repulsionForce = (overlap / requiredDistance) * 0.055 * this.config.antiClusteringStrength / 0.15; - blob1.velocityX -= normalizedDx * repulsionForce * force1Multiplier; - blob1.velocityY -= normalizedDy * repulsionForce * force1Multiplier; + const normalizedDx = dx / distance; + const normalizedDy = dy / distance; - blob2.velocityX += normalizedDx * repulsionForce * force2Multiplier; - blob2.velocityY += normalizedDy * repulsionForce * force2Multiplier; + const force1Multiplier = blob1.repulsionStrength || 0.03; + const force2Multiplier = blob2.repulsionStrength || 0.03; + + + const proximityMultiplier = distance < requiredDistance * 0.7 ? 3.5 : 1.0; + + blob1.velocityX -= normalizedDx * repulsionForce * force1Multiplier * proximityMultiplier; + blob1.velocityY -= normalizedDy * repulsionForce * force1Multiplier * proximityMultiplier; + + blob2.velocityX += normalizedDx * repulsionForce * force2Multiplier * proximityMultiplier; + blob2.velocityY += normalizedDy * repulsionForce * force2Multiplier * proximityMultiplier; - if (distance < requiredDistance) { blob1.lastRepulsionTime = Date.now(); blob2.lastRepulsionTime = Date.now(); } @@ -542,14 +484,8 @@ export class BlobPhysics { } private applyAccelerometerForces(blob: ConvexBlob): void { - const accelerometerStrength = 0.0008; - const maxForce = 0.003; - - const gravityX = Math.max(-maxForce, Math.min(maxForce, this.gravity.x * accelerometerStrength)); - const gravityY = Math.max(-maxForce, Math.min(maxForce, this.gravity.y * accelerometerStrength)); - - blob.velocityX += gravityX; - blob.velocityY += gravityY; + blob.velocityX += this.gravityField.x; + blob.velocityY += this.gravityField.y; if (blob.controlPoints && (Math.abs(this.gravity.x) > 0.3 || Math.abs(this.gravity.y) > 0.3)) { @@ -727,33 +663,36 @@ export class BlobPhysics { }); } - // Laplacian skin-tension pass on the perimeter ring. - // r_i ← r_i + k · (0.5·(r_{i-1} + r_{i+1}) - r_i) is the discrete - // surface-tension force on a closed control-point ring (Young-Laplace - // pressure). Two-pass: read all targets first, then write — otherwise - // we'd be smoothing against half-already-smoothed neighbors. - // Plus a viscous radial-velocity bleed (Kelvin-Voigt dashpot half) so - // energy dissipates with each correction rather than ringing as it - // did with the previous spring-only model. private smoothControlPoints(blob: ConvexBlob): void { - const cp = blob.controlPoints; - if (!cp || cp.length < 3) return; - const n = cp.length; - if (!this.skinTensionScratch || this.skinTensionScratch.length < n) { - this.skinTensionScratch = new Float32Array(n); - } - const target = this.skinTensionScratch; - const k = 0.15; + if (!blob.controlPoints || blob.controlPoints.length < 3) return; - for (let i = 0; i < n; i++) { - const prev = cp[(i - 1 + n) % n].radius; - const next = cp[(i + 1) % n].radius; - target[i] = 0.5 * (prev + next); + const controlPoints = blob.controlPoints; + const originalRadii = this.controlRadiusScratch; + const pointCount = controlPoints.length; + + for (let i = 0; i < pointCount; i++) { + originalRadii[i] = controlPoints[i].radius; } - for (let i = 0; i < n; i++) { - cp[i].radius += (target[i] - cp[i].radius) * k; - const v = blob.controlVelocities?.[i]; - if (v) v.radialVelocity *= 1 - 0.5 * k; + + for (let i = 0; i < pointCount; i++) { + const current = controlPoints[i]; + const prevRadius = originalRadii[(i - 1 + pointCount) % pointCount]; + const currentRadius = originalRadii[i]; + const nextRadius = originalRadii[(i + 1) % pointCount]; + + + const avgRadius = (prevRadius + currentRadius + nextRadius) / 3; + const smoothingFactor = 0.05; + current.radius = currentRadius * (1 - smoothingFactor) + avgRadius * smoothingFactor; + + + const minRadiusDiff = blob.size * 0.1; + const neighborRadius = (prevRadius + nextRadius) * 0.5; + const radiusDiff = current.radius - neighborRadius; + const excessRadiusDiff = Math.abs(radiusDiff) - minRadiusDiff; + if (excessRadiusDiff > 0) { + current.radius -= radiusDiff > 0 ? excessRadiusDiff * 0.5 : -excessRadiusDiff * 0.5; + } } } @@ -767,55 +706,36 @@ export class BlobPhysics { } } - // Soft-wall force: continuous penetration-based restoring force, no - // specular reflection or position snap. Edges deform along the wall - // (the blob "flattens") rather than bouncing — the gel cue. private handleWallBouncing(blob: ConvexBlob): void { const margin = blob.size * 0.8; - const yMargin = margin * 1.5; - const k = 0.08; + const damping = this.config.bounceDamping; const currentTime = Date.now(); - const minX = this.PHYSICS_MIN + margin; - const maxX = this.PHYSICS_MAX - margin; - const minY = this.PHYSICS_MIN + yMargin; - const maxY = this.PHYSICS_MAX - yMargin; - - const px = - Math.max(0, minX - blob.currentX) - Math.max(0, blob.currentX - maxX); - const py = - Math.max(0, minY - blob.currentY) - Math.max(0, blob.currentY - maxY); - - if (px !== 0) blob.velocityX += k * px; - if (py !== 0) blob.velocityY += k * py; - - // Hard outer clamp — far outside the soft band, snap back so the - // blob can never escape the canvas under extreme dt or large - // external forces. Records a bounce so existing time-since-bounce - // logic continues to work. - const hardMargin = blob.size * 0.2; - const hardMinX = this.PHYSICS_MIN + hardMargin; - const hardMaxX = this.PHYSICS_MAX - hardMargin; - const hardMinY = this.PHYSICS_MIN + hardMargin; - const hardMaxY = this.PHYSICS_MAX - hardMargin; - const hardDamping = this.config.bounceDamping; - - if (blob.currentX < hardMinX) { - blob.currentX = hardMinX; - blob.velocityX = Math.abs(blob.velocityX) * hardDamping; + + if (blob.currentX < this.PHYSICS_MIN + margin) { + blob.currentX = this.PHYSICS_MIN + margin; + blob.velocityX = Math.abs(blob.velocityX) * damping; this.recordBounce(blob, currentTime); - } else if (blob.currentX > hardMaxX) { - blob.currentX = hardMaxX; - blob.velocityX = -Math.abs(blob.velocityX) * hardDamping; + } + + + if (blob.currentX > this.PHYSICS_MAX - margin) { + blob.currentX = this.PHYSICS_MAX - margin; + blob.velocityX = -Math.abs(blob.velocityX) * damping; this.recordBounce(blob, currentTime); } - if (blob.currentY < hardMinY) { - blob.currentY = hardMinY; - blob.velocityY = Math.abs(blob.velocityY) * hardDamping; + + + if (blob.currentY < this.PHYSICS_MIN + margin * 1.5) { + blob.currentY = this.PHYSICS_MIN + margin * 1.5; + blob.velocityY = Math.abs(blob.velocityY) * damping; this.recordBounce(blob, currentTime); - } else if (blob.currentY > hardMaxY) { - blob.currentY = hardMaxY; - blob.velocityY = -Math.abs(blob.velocityY) * hardDamping; + } + + + if (blob.currentY > this.PHYSICS_MAX - margin * 1.5) { + blob.currentY = this.PHYSICS_MAX - margin * 1.5; + blob.velocityY = -Math.abs(blob.velocityY) * damping; this.recordBounce(blob, currentTime); } } diff --git a/src/core/InteractionField.ts b/src/core/InteractionField.ts new file mode 100644 index 0000000..62ec522 --- /dev/null +++ b/src/core/InteractionField.ts @@ -0,0 +1,89 @@ +export interface FieldVector { + x: number; + y: number; +} + +export interface PointFieldOptions { + origin: FieldVector; + target: FieldVector; + radius: number; + strength: number; +} + +const magnitude = (vector: FieldVector): number => + Math.sqrt(vector.x * vector.x + vector.y * vector.y); + +export function clampFieldVector(vector: FieldVector, maxMagnitude = 1): FieldVector { + const max = Math.max(0, maxMagnitude); + const currentMagnitude = magnitude(vector); + if (max === 0 || currentMagnitude === 0) return { x: 0, y: 0 }; + if (currentMagnitude <= max) return vector; + + const scale = max / currentMagnitude; + return { + x: vector.x * scale, + y: vector.y * scale, + }; +} + +export function combineFieldVectors( + fields: FieldVector[], + maxMagnitude = 1, +): FieldVector { + const total = fields.reduce( + (accumulator, field) => ({ + x: accumulator.x + field.x, + y: accumulator.y + field.y, + }), + { x: 0, y: 0 }, + ); + + return clampFieldVector(total, maxMagnitude); +} + +export function directionalBiasField( + input: FieldVector, + strength: number, + maxMagnitude = 1, +): FieldVector { + const max = Math.max(0, maxMagnitude); + const x = input.x * strength; + const y = input.y * strength; + const currentMagnitude = Math.sqrt(x * x + y * y); + + if (max === 0 || currentMagnitude === 0) return { x: 0, y: 0 }; + if (currentMagnitude <= max) return { x, y }; + + const scale = max / currentMagnitude; + return { + x: x * scale, + y: y * scale, + }; +} + +export function smoothDistanceFalloff(distance: number, radius: number): number { + const boundedDistance = Math.max(0, distance); + if (radius <= 0 || boundedDistance >= radius) return 0; + + const normalized = 1 - boundedDistance / radius; + return normalized * normalized; +} + +export function pointAttractorField({ + origin, + target, + radius, + strength, +}: PointFieldOptions): FieldVector { + const dx = target.x - origin.x; + const dy = target.y - origin.y; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance === 0) return { x: 0, y: 0 }; + + const falloff = smoothDistanceFalloff(distance, radius); + const scale = (falloff * strength) / distance; + return { + x: dx * scale, + y: dy * scale, + }; +} diff --git a/src/core/index.ts b/src/core/index.ts index 6dddcf9..c4264da 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -62,7 +62,7 @@ export type { BlendMode, ThemeColor, ThemePreset, -} from './schema.js'; +} from './theme-presets.js'; // — Render blob shapes — export type { @@ -88,11 +88,15 @@ export type { // — Theme presets and config — export { - DEFAULT_CONFIG, TRANS_THEME, PRIDE_THEME, TINYLAND_THEME, HIGH_CONTRAST_THEME, THEME_PRESETS, +} from './theme-presets.js'; +export { THEME_PRESET_COLORS } from './theme-colors.js'; + +export { + DEFAULT_CONFIG, mergeConfig, } from './schema.js'; diff --git a/src/core/schema.ts b/src/core/schema.ts index 1de1ac2..6873d06 100644 --- a/src/core/schema.ts +++ b/src/core/schema.ts @@ -7,6 +7,17 @@ import type { ControlPoint, ControlPointVelocity, DeviceMotionData } from './types.js'; +import type { + BlendMode, + ThemeColor, + ThemePresetName, +} from './theme-presets.js'; +export type { + BlendMode, + ThemeColor, + ThemePreset, + ThemePresetName, +} from './theme-presets.js'; @@ -166,53 +177,16 @@ export interface FeatureFlags { -export type ThemePresetName = 'tinyland' | 'trans' | 'pride' | 'high-contrast' | 'custom'; -export type BlendMode = 'multiply' | 'screen' | 'overlay' | 'soft-light' | 'normal'; -export interface ThemeColor { - - id: string; - - color: string; - - attractive: boolean; - - scrollAffinity: number; - - layer: 'background' | 'mid' | 'foreground'; -} - - - - -export interface ThemePreset { - - name: ThemePresetName; - - - label: string; - - - colors: ThemeColor[]; - - - blendModeLight: BlendMode; - - - blendModeDark: BlendMode; - - - hasVectors: boolean; -} @@ -510,89 +484,14 @@ export const DEFAULT_CONFIG: TinyVectorsConfig = { -export const TRANS_THEME: ThemePreset = { - name: 'trans', - label: 'Trans Pride', - hasVectors: true, - blendModeLight: 'multiply', - blendModeDark: 'screen', - colors: [ - { id: 'trans-blue', color: 'rgba(91, 206, 250, 0.60)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, - { id: 'trans-pink', color: 'rgba(245, 169, 184, 0.65)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, - { id: 'trans-white', color: 'rgba(242, 242, 245, 0.50)', attractive: false, scrollAffinity: 0.5, layer: 'mid' }, - { id: 'trans-sky-blue', color: 'rgba(170, 225, 250, 0.55)', attractive: false, scrollAffinity: 0.6, layer: 'mid' }, - { id: 'trans-powder-blue', color: 'rgba(160, 190, 255, 0.65)', attractive: true, scrollAffinity: 0.7, layer: 'foreground' }, - { id: 'trans-rose-pink', color: 'rgba(250, 200, 210, 0.55)', attractive: false, scrollAffinity: 0.6, layer: 'mid' }, - { id: 'trans-blush-pink', color: 'rgba(255, 160, 220, 0.65)', attractive: true, scrollAffinity: 0.7, layer: 'foreground' }, - { id: 'trans-lavender', color: 'rgba(220, 220, 255, 0.55)', attractive: false, scrollAffinity: 0.5, layer: 'background' }, - ], -}; - - - - -export const PRIDE_THEME: ThemePreset = { - name: 'pride', - label: 'Pride Rainbow', - hasVectors: true, - blendModeLight: 'multiply', - blendModeDark: 'screen', - colors: [ - { id: 'pride-red', color: 'rgba(228, 3, 3, 0.55)', attractive: true, scrollAffinity: 0.9, layer: 'foreground' }, - { id: 'pride-orange', color: 'rgba(255, 140, 0, 0.55)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, - { id: 'pride-yellow', color: 'rgba(255, 237, 0, 0.55)', attractive: false, scrollAffinity: 0.7, layer: 'mid' }, - { id: 'pride-green', color: 'rgba(0, 128, 38, 0.55)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, - { id: 'pride-blue', color: 'rgba(36, 64, 142, 0.55)', attractive: true, scrollAffinity: 0.9, layer: 'foreground' }, - { id: 'pride-purple', color: 'rgba(115, 41, 130, 0.55)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, - ], -}; - - - - -export const TINYLAND_THEME: ThemePreset = { - name: 'tinyland', - label: 'Tinyland', - hasVectors: true, - blendModeLight: 'multiply', - blendModeDark: 'screen', - colors: [ - { id: 'tinyland-purple', color: 'rgba(139, 92, 246, 0.55)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, - { id: 'tinyland-blue', color: 'rgba(59, 130, 246, 0.55)', attractive: true, scrollAffinity: 0.7, layer: 'foreground' }, - { id: 'tinyland-pink', color: 'rgba(236, 72, 153, 0.50)', attractive: true, scrollAffinity: 0.8, layer: 'mid' }, - { id: 'tinyland-white', color: 'rgba(242, 242, 245, 0.45)', attractive: false, scrollAffinity: 0.4, layer: 'background' }, - ], -}; - - - - -export const HIGH_CONTRAST_THEME: ThemePreset = { - name: 'high-contrast', - label: 'High Contrast', - hasVectors: false, - blendModeLight: 'normal', - blendModeDark: 'normal', - colors: [], -}; - - - - -export const THEME_PRESETS: Record = { - tinyland: TINYLAND_THEME, - trans: TRANS_THEME, - pride: PRIDE_THEME, - 'high-contrast': HIGH_CONTRAST_THEME, - custom: { - name: 'custom', - label: 'Custom', - hasVectors: true, - blendModeLight: 'multiply', - blendModeDark: 'screen', - colors: [], - }, -}; +export { + HIGH_CONTRAST_THEME, + PRIDE_THEME, + THEME_PRESETS, + TINYLAND_THEME, + TRANS_THEME, +} from './theme-presets.js'; +export { THEME_PRESET_COLORS } from './theme-colors.js'; diff --git a/src/core/theme-colors.ts b/src/core/theme-colors.ts new file mode 100644 index 0000000..46029c7 --- /dev/null +++ b/src/core/theme-colors.ts @@ -0,0 +1,30 @@ +import type { ThemePresetName } from './theme-presets.js'; + +export const THEME_PRESET_COLORS: Record = { + tinyland: [ + 'rgba(139,92,246,.55)', + 'rgba(59,130,246,.55)', + 'rgba(236,72,153,.50)', + 'rgba(242,242,245,.45)', + ], + trans: [ + 'rgba(91,206,250,.60)', + 'rgba(245,169,184,.65)', + 'rgba(242,242,245,.50)', + 'rgba(170,225,250,.55)', + 'rgba(160,190,255,.65)', + 'rgba(250,200,210,.55)', + 'rgba(255,160,220,.65)', + 'rgba(220,220,255,.55)', + ], + pride: [ + 'rgba(228,3,3,.55)', + 'rgba(255,140,0,.55)', + 'rgba(255,237,0,.55)', + 'rgba(0,128,38,.55)', + 'rgba(36,64,142,.55)', + 'rgba(115,41,130,.55)', + ], + 'high-contrast': [], + custom: [], +}; diff --git a/src/core/theme-presets.ts b/src/core/theme-presets.ts new file mode 100644 index 0000000..02be596 --- /dev/null +++ b/src/core/theme-presets.ts @@ -0,0 +1,92 @@ +export type ThemePresetName = 'tinyland' | 'trans' | 'pride' | 'high-contrast' | 'custom'; + +export type BlendMode = 'multiply' | 'screen' | 'overlay' | 'soft-light' | 'normal'; + +export interface ThemeColor { + id: string; + color: string; + attractive: boolean; + scrollAffinity: number; + layer: 'background' | 'mid' | 'foreground'; +} + +export interface ThemePreset { + name: ThemePresetName; + label: string; + colors: ThemeColor[]; + blendModeLight: BlendMode; + blendModeDark: BlendMode; + hasVectors: boolean; +} + +export const TRANS_THEME: ThemePreset = { + name: 'trans', + label: 'Trans Pride', + hasVectors: true, + blendModeLight: 'multiply', + blendModeDark: 'screen', + colors: [ + { id: 'trans-blue', color: 'rgba(91, 206, 250, 0.60)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, + { id: 'trans-pink', color: 'rgba(245, 169, 184, 0.65)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, + { id: 'trans-white', color: 'rgba(242, 242, 245, 0.50)', attractive: false, scrollAffinity: 0.5, layer: 'mid' }, + { id: 'trans-sky-blue', color: 'rgba(170, 225, 250, 0.55)', attractive: false, scrollAffinity: 0.6, layer: 'mid' }, + { id: 'trans-powder-blue', color: 'rgba(160, 190, 255, 0.65)', attractive: true, scrollAffinity: 0.7, layer: 'foreground' }, + { id: 'trans-rose-pink', color: 'rgba(250, 200, 210, 0.55)', attractive: false, scrollAffinity: 0.6, layer: 'mid' }, + { id: 'trans-blush-pink', color: 'rgba(255, 160, 220, 0.65)', attractive: true, scrollAffinity: 0.7, layer: 'foreground' }, + { id: 'trans-lavender', color: 'rgba(220, 220, 255, 0.55)', attractive: false, scrollAffinity: 0.5, layer: 'background' }, + ], +}; + +export const PRIDE_THEME: ThemePreset = { + name: 'pride', + label: 'Pride Rainbow', + hasVectors: true, + blendModeLight: 'multiply', + blendModeDark: 'screen', + colors: [ + { id: 'pride-red', color: 'rgba(228, 3, 3, 0.55)', attractive: true, scrollAffinity: 0.9, layer: 'foreground' }, + { id: 'pride-orange', color: 'rgba(255, 140, 0, 0.55)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, + { id: 'pride-yellow', color: 'rgba(255, 237, 0, 0.55)', attractive: false, scrollAffinity: 0.7, layer: 'mid' }, + { id: 'pride-green', color: 'rgba(0, 128, 38, 0.55)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, + { id: 'pride-blue', color: 'rgba(36, 64, 142, 0.55)', attractive: true, scrollAffinity: 0.9, layer: 'foreground' }, + { id: 'pride-purple', color: 'rgba(115, 41, 130, 0.55)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, + ], +}; + +export const TINYLAND_THEME: ThemePreset = { + name: 'tinyland', + label: 'Tinyland', + hasVectors: true, + blendModeLight: 'multiply', + blendModeDark: 'screen', + colors: [ + { id: 'tinyland-purple', color: 'rgba(139, 92, 246, 0.55)', attractive: true, scrollAffinity: 0.8, layer: 'foreground' }, + { id: 'tinyland-blue', color: 'rgba(59, 130, 246, 0.55)', attractive: true, scrollAffinity: 0.7, layer: 'foreground' }, + { id: 'tinyland-pink', color: 'rgba(236, 72, 153, 0.50)', attractive: true, scrollAffinity: 0.8, layer: 'mid' }, + { id: 'tinyland-white', color: 'rgba(242, 242, 245, 0.45)', attractive: false, scrollAffinity: 0.4, layer: 'background' }, + ], +}; + +export const HIGH_CONTRAST_THEME: ThemePreset = { + name: 'high-contrast', + label: 'High Contrast', + hasVectors: false, + blendModeLight: 'normal', + blendModeDark: 'normal', + colors: [], +}; + +export const THEME_PRESETS: Record = { + tinyland: TINYLAND_THEME, + trans: TRANS_THEME, + pride: PRIDE_THEME, + 'high-contrast': HIGH_CONTRAST_THEME, + custom: { + name: 'custom', + label: 'Custom', + hasVectors: true, + blendModeLight: 'multiply', + blendModeDark: 'screen', + colors: [], + }, +}; diff --git a/src/index.ts b/src/index.ts index 0578c05..9bbf0fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,30 +1,109 @@ +// Curated public package root. Keep this explicit so the npm surface stays +// reviewable and tree-shake-friendly. - - - - - -export * from './core/index.js'; - - -export * from './motion/index.js'; - - -export * from './themes/index.js'; - - -export * from './svelte/index.js'; - +export { + BlobPhysics, + generateSmoothBlobPath, + generateSmoothBlobPathSync, + preInitPathGenerator, + SpringSystem, + DEFAULT_SPRING_CONFIG, + computePolygonArea, + computeCircularity, + enforceAreaConservation, + createControlPointVelocities, + GaussianKernel, + SpatialHash, + browser, + isBrowser, + TRANS_THEME, + PRIDE_THEME, + TINYLAND_THEME, + HIGH_CONTRAST_THEME, + THEME_PRESETS, + THEME_PRESET_COLORS, + DEFAULT_CONFIG, + mergeConfig, +} from './core/index.js'; export type { + BlobPhysicsConfig, + SpringConfig, + GelControlPoint, + ControlPoint, + ControlPointVelocity, + ConvexBlob, + ColorDefinition, + DeviceMotionData, + GravityVector, + TiltVector, TinyVectorsConfig, CoreConfig, PhysicsConfig, RenderingConfig, ThemeConfig, FeatureFlags, - RenderBlob, + TinyVectorsConfigOverride, + DeepPartial, + ThemePresetName, + BlendMode, ThemeColor, ThemePreset, - ThemePresetName, -} from './core/schema.js'; + BlobCore, + RenderBlob, + PhysicsBlob, + ScrollData, + PointerData, + TinyVectorsEventType, + TinyVectorsEventHandler, + TinyVectorsEvent, + FrameEventData, + ThemeChangeEventData, +} from './core/index.js'; + +export { + DeviceMotion, + mapClientPointToPhysics, + createPointerPhysicsController, + detectPointerPhysicsCapability, + getLatestPointerEvent, + ScrollHandler, +} from './motion/index.js'; + +export type { + DeviceMotionCallback, + DeviceMotionOptions, + DeviceMotionPermissionState, + MotionVector, + PhysicsPoint, + PhysicsRange, + PointerBounds, + PointerCapabilityEnvironment, + PointerCancelEventName, + PointerExitEventName, + PointerExitLikeEvent, + PointerLifecycleEventName, + PointerLikeEvent, + PointerMoveEventName, + PointerPhysicsController, + PointerPhysicsControllerOptions, + PointerPhysicsEventTarget, + ScrollHandlerConfig, + PullForce, +} from './motion/index.js'; + +export { + getThemePreset, + generateThemeCSS, + isDarkMode, + watchDarkMode, +} from './themes/index.js'; + +export { + TinyVectors, + BlobSVG, +} from './svelte/index.js'; + +export type { + TinyVectorsDeviceMotionStatus, +} from './svelte/index.js'; diff --git a/src/motion/DeviceMotion.ts b/src/motion/DeviceMotion.ts index c627f5d..70ef5ef 100644 --- a/src/motion/DeviceMotion.ts +++ b/src/motion/DeviceMotion.ts @@ -1,28 +1,55 @@ import { OneEuro } from './OneEuro.js'; -export type DeviceMotionCallback = (data: { +export interface MotionVector { x: number; y: number; z: number; -}) => void; +} + +export type DeviceMotionCallback = (data: MotionVector) => void; + +export type DeviceMotionPermissionState = + | 'unknown' + | 'unsupported' + | 'insecure' + | 'prompt' + | 'granted' + | 'denied'; export interface DeviceMotionOptions { /** One-Euro min cutoff (Hz). Lower = smoother at rest. Default 0.5. */ oneEuroMinCutoff?: number; - /** One-Euro speed-responsiveness. Default 0.01 (low for ambient). */ + /** One-Euro speed responsiveness. Default 0.01 for ambient backgrounds. */ oneEuroBeta?: number; /** One-Euro speed-estimate cutoff (Hz). Default 1.0. */ oneEuroDCutoff?: number; - /** Slow continuous baseline EMA. Default 0.0008 (~30 s τ). */ + /** Slow continuous baseline EMA. Default 0.0008, roughly 30 s tau. */ baselineAlpha?: number; - /** Discard events for the first N ms after first sample. Default 250. */ + /** Discard events for the first N ms after listener startup. Default 250. */ warmupMs?: number; - /** Suppress output when |beta| exceeds this. Default 120°. */ + /** Suppress output when |beta| exceeds this. Default 120 degrees. */ faceDownThreshold?: number; - /** Reset filter/baseline if event gap exceeds this. Default 2000 ms. */ + /** Reset filters if event gap exceeds this. Default 2000 ms. */ staleEventMs?: number; - /** Degrees mapped to ±1. Default 45 (matches casual tilt range). */ + /** Emit neutral output if events stop for this long. Default 2000 ms. */ + idleResetMs?: number; + /** Degrees mapped to +/-1. Default 45, matching casual tilt range. */ range?: number; + /** Manual calibration sample count used by calibrate(). Default 8. */ + calibrationSamples?: number; + /** Values smaller than this are treated as rest-state noise. Default 0.015. */ + deadZone?: number; +} + +interface MotionWindow { + DeviceOrientationEvent?: { + requestPermission?: () => Promise<'granted' | 'denied'>; + }; +} + +interface LegacyMediaQueryList { + addListener?: (listener: () => void) => void; + removeListener?: (listener: () => void) => void; } const DEFAULTS = { @@ -33,17 +60,15 @@ const DEFAULTS = { warmupMs: 250, faceDownThreshold: 120, staleEventMs: 2000, + idleResetMs: 2000, range: 45, + calibrationSamples: 8, + deadZone: 0.015, } satisfies Required; -// Convert raw (beta, gamma) → screen-aligned (sx, sy) given -// screen.orientation.angle. sx is "left-right tilt felt by the user", -// sy is "front-back tilt felt by the user". Pure for unit tests. -export function remapToScreen( - beta: number, - gamma: number, - angle: number -): [number, number] { +// Convert raw (beta, gamma) to screen-aligned tilt. sx is left/right +// tilt as felt by the user; sy is front/back tilt as felt by the user. +export function remapToScreen(beta: number, gamma: number, angle: number): [number, number] { switch (angle) { case 90: return [beta, -gamma]; @@ -57,205 +82,392 @@ export function remapToScreen( } } -function clamp(v: number, lo: number, hi: number): number { - return v < lo ? lo : v > hi ? hi : v; +function clamp(value: number, min = -1, max = 1): number { + return Math.max(min, Math.min(max, value)); +} + +function applyDeadZone(value: number, deadZone: number): number { + return Math.abs(value) < deadZone ? 0 : value; } -type OrientationPermission = () => Promise; -function getPermissionApi(): OrientationPermission | null { - if (typeof DeviceOrientationEvent === 'undefined') return null; - const fn = ( - DeviceOrientationEvent as unknown as { requestPermission?: OrientationPermission } - ).requestPermission; - return typeof fn === 'function' ? fn : null; +function getPermissionApi(): (() => Promise<'granted' | 'denied'>) | null { + if (typeof window === 'undefined') return null; + const constructor = (window as unknown as MotionWindow).DeviceOrientationEvent; + const requestPermission = constructor?.requestPermission; + return typeof requestPermission === 'function' ? requestPermission.bind(constructor) : null; +} + +export function getDeviceMotionCapabilityState(): DeviceMotionPermissionState { + if (typeof window === 'undefined') return 'unsupported'; + if (!window.isSecureContext) return 'insecure'; + return 'DeviceOrientationEvent' in window ? 'unknown' : 'unsupported'; +} + +export function isDeviceMotionPermissionRequired(): boolean { + return getPermissionApi() !== null; +} + +function getScreenOrientationAngle(): number { + if (typeof screen === 'undefined') return 0; + return screen.orientation?.angle ?? 0; +} + +function addMediaQueryChangeListener(mql: MediaQueryList, listener: () => void): void { + if (typeof mql.addEventListener === 'function') { + mql.addEventListener('change', listener); + return; + } + + (mql as LegacyMediaQueryList).addListener?.(listener); +} + +function removeMediaQueryChangeListener(mql: MediaQueryList, listener: () => void): void { + if (typeof mql.removeEventListener === 'function') { + mql.removeEventListener('change', listener); + return; + } + + (mql as LegacyMediaQueryList).removeListener?.(listener); } -// Replaces the previous raw-acceleration DeviceMotion implementation. -// Listens to DeviceOrientationEvent (OS-fused, low-noise) instead of -// DeviceMotionEvent.accelerationIncludingGravity. Filters with One-Euro -// for an ambient-feel adaptive low-pass; subtracts a slow baseline so -// resting pose (cable bias, pocket lean) is absorbed without killing -// gravity feel. Honors prefers-reduced-motion as a hard disable. export class DeviceMotion { - private callback: DeviceMotionCallback; - private opts: Required; + private readonly callback: DeviceMotionCallback; + private readonly opts: Required; + private readonly filterX: OneEuro; + private readonly filterY: OneEuro; + private permissionState: DeviceMotionPermissionState = 'unknown'; private isListening = false; private disposed = false; - private filterX: OneEuro; - private filterY: OneEuro; + private listenerStartedAt = 0; + private lastEventAt = 0; private baseX = 0; private baseY = 0; - private firstEventAt = 0; - private lastEventAt = 0; - private boundOrientation: ((e: DeviceOrientationEvent) => void) | null = null; + private lastScreen: { x: number; y: number } | null = null; + private calibrationRemaining = 0; + private calibrationTargetSamples = 0; + private calibrationTotalX = 0; + private calibrationTotalY = 0; + private boundOrientation: ((event: DeviceOrientationEvent) => void) | null = null; private boundVisibility: (() => void) | null = null; private reducedMotionMql: MediaQueryList | null = null; private reducedMotionListener: (() => void) | null = null; + private blockedByReducedMotion = false; + private idleResetTimer: ReturnType | null = null; constructor(callback: DeviceMotionCallback, options: DeviceMotionOptions = {}) { this.callback = callback; this.opts = { ...DEFAULTS, ...options }; - const eu = { + const params = { minCutoff: this.opts.oneEuroMinCutoff, beta: this.opts.oneEuroBeta, dCutoff: this.opts.oneEuroDCutoff, }; - this.filterX = new OneEuro(eu); - this.filterY = new OneEuro(eu); + this.filterX = new OneEuro(params); + this.filterY = new OneEuro(params); } - async initialize(): Promise { - if (this.disposed) return; - if (typeof window === 'undefined') return; - if (!window.isSecureContext) { - console.warn('DeviceMotion APIs require a secure context (HTTPS)'); - return; + async initialize(): Promise { + if (!this.detectSupport()) return false; + this.observeReducedMotion(); + + if (this.prefersReducedMotion()) { + this.blockedByReducedMotion = true; + this.permissionState = 'denied'; + this.stopListening(); + return false; + } + + if (getPermissionApi()) { + this.permissionState = 'prompt'; + return false; } - if (!('DeviceOrientationEvent' in window)) { - console.log('DeviceOrientationEvent not supported'); + + this.permissionState = 'granted'; + this.startListening(); + return true; + } + + async requestPermission(): Promise { + if (!this.detectSupport()) return false; + this.observeReducedMotion(); + + if (this.prefersReducedMotion()) { + this.blockedByReducedMotion = true; + this.permissionState = 'denied'; + this.stopListening(); + return false; + } + + const requestPermission = getPermissionApi(); + if (requestPermission) { + this.permissionState = 'prompt'; + try { + this.permissionState = await requestPermission(); + } catch { + this.permissionState = 'denied'; + } + } else { + this.permissionState = 'granted'; + } + + if (this.disposed) { + this.stopListening(); + return false; + } + + if (this.prefersReducedMotion()) { + this.blockedByReducedMotion = true; + this.stopListening(); + this.resetFilterState(); + this.emitNeutral(); + return false; + } + + if (this.permissionState !== 'granted') { + this.stopListening(); + return false; + } + + this.startListening(); + return true; + } + + /** + * Re-zero tilt from the next N orientation samples. Calibration samples are + * consumed for the baseline only; normal output resumes on the following event. + */ + calibrate(samples = this.opts.calibrationSamples): void { + const sampleCount = Math.max(0, Math.floor(samples)); + this.resetFilterState({ resetWarmup: false }); + + if (sampleCount === 0) { + if (this.lastScreen) { + this.baseX = this.lastScreen.x; + this.baseY = this.lastScreen.y; + } + this.calibrationRemaining = 0; return; } - // prefers-reduced-motion is a hard disable. Subscribe to changes so - // we honor a runtime toggle (Apple users can flip this from Control - // Center) — but never auto-listen until explicitly initialized. - this.reducedMotionMql = - window.matchMedia?.('(prefers-reduced-motion: reduce)') ?? null; + this.calibrationRemaining = sampleCount; + this.calibrationTargetSamples = sampleCount; + this.calibrationTotalX = 0; + this.calibrationTotalY = 0; + } + + getPermissionState(): DeviceMotionPermissionState { + return this.permissionState; + } + + isActive(): boolean { + return this.isListening; + } + + cleanup(): void { + this.disposed = true; + this.stopListening(); + + if (this.reducedMotionMql && this.reducedMotionListener) { + removeMediaQueryChangeListener(this.reducedMotionMql, this.reducedMotionListener); + } + this.reducedMotionMql = null; + this.reducedMotionListener = null; + this.blockedByReducedMotion = false; + this.resetFilterState(); + } + + private detectSupport(): boolean { + if (this.disposed) return false; + + const capabilityState = getDeviceMotionCapabilityState(); + if (capabilityState !== 'unknown') { + this.permissionState = capabilityState; + return false; + } + + return true; + } + + private observeReducedMotion(): void { + if (this.reducedMotionMql || typeof window === 'undefined') return; + + this.reducedMotionMql = window.matchMedia?.('(prefers-reduced-motion: reduce)') ?? null; this.reducedMotionListener = () => { if (this.disposed || !this.reducedMotionMql) return; - if (this.reducedMotionMql.matches && this.isListening) { + + if (this.reducedMotionMql.matches) { + this.blockedByReducedMotion = true; this.stopListening(); - } else if (!this.reducedMotionMql.matches && !this.isListening) { - // Re-engage if user disabled reduced-motion mid-session. - // requestPermission() handles the no-API case internally. - // Guard against post-cleanup resolution: iOS may have a - // permission prompt open when cleanup() fires. - void this.requestPermission().then((ok) => { - if (ok && !this.disposed) this.startListening(); - }); + this.resetFilterState(); + this.emitNeutral(); + return; } - }; - this.reducedMotionMql?.addEventListener('change', this.reducedMotionListener); - if (this.reducedMotionMql?.matches) return; + if (this.blockedByReducedMotion) { + this.blockedByReducedMotion = false; + if (this.permissionState === 'granted' || !getPermissionApi()) { + this.permissionState = 'granted'; + this.startListening(); + return; + } + this.permissionState = 'prompt'; + return; + } - const ok = await this.requestPermission(); - if (ok && !this.disposed) this.startListening(); + if (this.permissionState === 'granted') { + this.startListening(); + } + }; + if (this.reducedMotionMql) { + addMediaQueryChangeListener(this.reducedMotionMql, this.reducedMotionListener); + } } - async requestPermission(): Promise { - const api = getPermissionApi(); - if (!api) return true; - try { - const r = await api(); - return r === 'granted'; - } catch (err) { - console.error('Error requesting device orientation permission:', err); - return false; - } + private prefersReducedMotion(): boolean { + return this.reducedMotionMql?.matches ?? false; } private startListening(): void { - if (this.isListening) return; - this.boundOrientation = (e: DeviceOrientationEvent) => this.handle(e); - window.addEventListener('deviceorientation', this.boundOrientation, { - passive: true, - } as AddEventListenerOptions); + if (this.disposed || this.isListening || typeof window === 'undefined') return; + + this.boundOrientation = (event: DeviceOrientationEvent) => this.handleOrientation(event); + window.addEventListener('deviceorientation', this.boundOrientation, { passive: true }); this.boundVisibility = () => { - if (document.hidden) this.resetFilterState(); + if (document.hidden) { + this.resetFilterState(); + this.emitNeutral(); + } }; document.addEventListener('visibilitychange', this.boundVisibility); + this.listenerStartedAt = this.now(); + this.lastEventAt = 0; this.isListening = true; + this.armIdleReset(); } private stopListening(): void { if (!this.isListening) return; + this.clearIdleReset(); + if (this.boundOrientation) { window.removeEventListener('deviceorientation', this.boundOrientation); this.boundOrientation = null; } + if (this.boundVisibility) { document.removeEventListener('visibilitychange', this.boundVisibility); this.boundVisibility = null; } - this.isListening = false; - } - private resetFilterState(): void { - this.filterX.reset(); - this.filterY.reset(); - this.firstEventAt = 0; - this.lastEventAt = 0; + this.isListening = false; } - private handle(event: DeviceOrientationEvent): void { - if (event.beta == null || event.gamma == null) return; - - const now = - typeof performance !== 'undefined' ? performance.now() : Date.now(); + private handleOrientation(event: DeviceOrientationEvent): void { + if (this.disposed || event.beta == null || event.gamma == null) return; - if (this.firstEventAt === 0) this.firstEventAt = now; - if (now - this.firstEventAt < this.opts.warmupMs) return; + const now = this.now(); + if (now - this.listenerStartedAt < this.opts.warmupMs) return; - if ( - this.lastEventAt > 0 && - now - this.lastEventAt > this.opts.staleEventMs - ) { + if (this.lastEventAt > 0 && now - this.lastEventAt > this.opts.staleEventMs) { this.resetFilterState(); - this.firstEventAt = now; + this.listenerStartedAt = now; this.lastEventAt = now; + this.emitNeutral(); + this.armIdleReset(); return; } this.lastEventAt = now; - // Face-down or upside-down: emit zero rather than wild values. if (Math.abs(event.beta) > this.opts.faceDownThreshold) { - this.callback({ x: 0, y: 0, z: 0 }); + this.emitNeutral(); + this.armIdleReset(); return; } - const angle = - (typeof screen !== 'undefined' && screen.orientation?.angle) || 0; - const [sx, sy] = remapToScreen(event.beta, event.gamma, angle); + const [screenX, screenY] = remapToScreen( + event.beta, + event.gamma, + getScreenOrientationAngle(), + ); + this.lastScreen = { x: screenX, y: screenY }; - // Slow continuous baseline absorbs cable bias / pocket lean over - // ~30 s without killing gravity feel. - const a = this.opts.baselineAlpha; - this.baseX += a * (sx - this.baseX); - this.baseY += a * (sy - this.baseY); + if (!this.consumeCalibrationSample(screenX, screenY)) { + this.armIdleReset(); + return; + } - const range = this.opts.range; - const xRaw = (sx - this.baseX) / range; - const yRaw = (sy - this.baseY) / range; + const alpha = this.opts.baselineAlpha; + this.baseX += alpha * (screenX - this.baseX); + this.baseY += alpha * (screenY - this.baseY); + const xRaw = (screenX - this.baseX) / this.opts.range; + const yRaw = (screenY - this.baseY) / this.opts.range; const xFiltered = this.filterX.filter(xRaw, now); const yFiltered = this.filterY.filter(yRaw, now); this.callback({ - x: clamp(xFiltered, -1, 1), - y: clamp(yFiltered, -1, 1), + x: applyDeadZone(clamp(xFiltered), this.opts.deadZone), + y: applyDeadZone(clamp(yFiltered), this.opts.deadZone), z: 0, }); + this.armIdleReset(); } - cleanup(): void { - // Set disposed first so any in-flight requestPermission() promise - // that resolves after cleanup() short-circuits before re-attaching - // a deviceorientation listener (iOS keeps the permission prompt - // open across tab navigation; the user can dismiss after the - // component has unmounted). - this.disposed = true; - this.stopListening(); - if (this.reducedMotionMql && this.reducedMotionListener) { - this.reducedMotionMql.removeEventListener( - 'change', - this.reducedMotionListener - ); + private consumeCalibrationSample(screenX: number, screenY: number): boolean { + if (this.calibrationRemaining <= 0) return true; + + this.calibrationTotalX += screenX; + this.calibrationTotalY += screenY; + this.calibrationRemaining -= 1; + + if (this.calibrationRemaining > 0) return false; + + const sampleCount = Math.max(1, this.calibrationTargetSamples); + this.baseX = this.calibrationTotalX / sampleCount; + this.baseY = this.calibrationTotalY / sampleCount; + this.calibrationTotalX = 0; + this.calibrationTotalY = 0; + this.resetFilterState({ resetWarmup: false }); + return false; + } + + private resetFilterState({ resetWarmup = true } = {}): void { + this.filterX.reset(); + this.filterY.reset(); + if (resetWarmup) { + this.listenerStartedAt = this.now(); } - this.reducedMotionMql = null; - this.reducedMotionListener = null; - this.resetFilterState(); + this.lastEventAt = 0; + } + + private emitNeutral(): void { + this.callback({ x: 0, y: 0, z: 0 }); + } + + private armIdleReset(): void { + this.clearIdleReset(); + if (!this.isListening || this.opts.idleResetMs <= 0) return; + + this.idleResetTimer = setTimeout(() => { + this.idleResetTimer = null; + if (this.disposed || !this.isListening) return; + + this.resetFilterState({ resetWarmup: false }); + this.emitNeutral(); + }, this.opts.idleResetMs); + + (this.idleResetTimer as { unref?: () => void }).unref?.(); + } + + private clearIdleReset(): void { + if (this.idleResetTimer === null) return; + clearTimeout(this.idleResetTimer); + this.idleResetTimer = null; + } + + private now(): number { + return typeof performance !== 'undefined' ? performance.now() : Date.now(); } } diff --git a/src/motion/OneEuro.ts b/src/motion/OneEuro.ts index da61364..d7b9348 100644 --- a/src/motion/OneEuro.ts +++ b/src/motion/OneEuro.ts @@ -31,7 +31,11 @@ export class OneEuro { private prevX: number | undefined; private prevT: number | undefined; - constructor(private p: OneEuroParams) {} + constructor(private p: OneEuroParams) { + if (p.minCutoff <= 0 || p.dCutoff <= 0) { + throw new RangeError('OneEuro: minCutoff and dCutoff must be > 0'); + } + } /** Filter sample `x` at time `tMs` (milliseconds). */ filter(x: number, tMs: number): number { @@ -40,6 +44,7 @@ export class OneEuro { this.prevX = x; return this.x.filter(x, 1); } + // tMs must be monotonically non-decreasing; backward jumps clamp to 1 ms. const dt = Math.max(1e-3, (tMs - this.prevT) / 1000); const dxRaw = (x - this.prevX) / dt; const dxHat = this.dx.filter(dxRaw, alpha(this.p.dCutoff, dt)); diff --git a/src/motion/PointerMapper.ts b/src/motion/PointerMapper.ts new file mode 100644 index 0000000..adb48e3 --- /dev/null +++ b/src/motion/PointerMapper.ts @@ -0,0 +1,43 @@ +export interface PointerBounds { + left: number; + top: number; + width: number; + height: number; +} + +export interface PhysicsPoint { + x: number; + y: number; +} + +export interface PhysicsRange { + min: number; + max: number; +} + +const DEFAULT_RANGE: PhysicsRange = { min: 0, max: 100 }; + +const clamp01 = (value: number): number => Math.max(0, Math.min(1, value)); + +export function mapClientPointToPhysics( + clientX: number, + clientY: number, + bounds: PointerBounds, + range: PhysicsRange = DEFAULT_RANGE, +): PhysicsPoint { + if (bounds.width <= 0 || bounds.height <= 0) { + return { + x: (range.min + range.max) / 2, + y: (range.min + range.max) / 2, + }; + } + + const span = range.max - range.min; + const normalizedX = clamp01((clientX - bounds.left) / bounds.width); + const normalizedY = clamp01((clientY - bounds.top) / bounds.height); + + return { + x: range.min + normalizedX * span, + y: range.min + normalizedY * span, + }; +} diff --git a/src/motion/PointerPhysicsController.ts b/src/motion/PointerPhysicsController.ts new file mode 100644 index 0000000..69a7e3b --- /dev/null +++ b/src/motion/PointerPhysicsController.ts @@ -0,0 +1,173 @@ +import { + mapClientPointToPhysics, + type PhysicsPoint, + type PhysicsRange, + type PointerBounds, +} from './PointerMapper.js'; + +export type PointerMoveEventName = 'pointermove' | 'mousemove'; +export type PointerExitEventName = 'pointerout' | 'mouseout'; +export type PointerCancelEventName = 'pointercancel'; +export type PointerLifecycleEventName = + | PointerMoveEventName + | PointerExitEventName + | PointerCancelEventName + | 'blur'; + +export interface PointerPhysicsEventTarget { + addEventListener( + type: PointerLifecycleEventName, + listener: EventListener, + options?: AddEventListenerOptions, + ): void; + removeEventListener(type: PointerLifecycleEventName, listener: EventListener): void; +} + +export interface PointerLikeEvent { + clientX: number; + clientY: number; + getCoalescedEvents?: () => PointerLikeEvent[]; +} + +export interface PointerExitLikeEvent { + relatedTarget?: EventTarget | null; +} + +export interface PointerPhysicsControllerOptions { + target: PointerPhysicsEventTarget; + getBounds: () => PointerBounds; + updatePosition: (position: PhysicsPoint) => void; + range?: PhysicsRange; + supportsPointerEvents?: boolean; + requestFrame?: (callback: FrameRequestCallback) => number; + cancelFrame?: (handle: number) => void; +} + +export interface PointerCapabilityEnvironment { + PointerEvent?: unknown; + MouseEvent?: unknown; + navigator?: { + maxTouchPoints?: number; + }; + matchMedia?: (query: string) => { matches: boolean }; +} + +export interface PointerPhysicsController { + readonly eventName: PointerMoveEventName; + readonly exitEventName: PointerExitEventName; + readonly cancelEventName: PointerCancelEventName | null; + flush(): void; + dispose(): void; +} + +export function getLatestPointerEvent(event: PointerLikeEvent): PointerLikeEvent { + const coalesced = + typeof event.getCoalescedEvents === 'function' ? event.getCoalescedEvents() : []; + return coalesced.length > 0 ? coalesced[coalesced.length - 1] : event; +} + +export function detectPointerPhysicsCapability( + environment: PointerCapabilityEnvironment = globalThis, +): boolean { + if (typeof environment.PointerEvent !== 'undefined') return true; + if ((environment.navigator?.maxTouchPoints ?? 0) > 0) return true; + if (environment.matchMedia?.('(pointer: fine), (pointer: coarse)').matches) return true; + return typeof environment.MouseEvent !== 'undefined'; +} + +function getRangeCenter(range?: PhysicsRange): PhysicsPoint { + const center = range ? (range.min + range.max) / 2 : 50; + return { x: center, y: center }; +} + +export function createPointerPhysicsController( + options: PointerPhysicsControllerOptions, +): PointerPhysicsController { + const requestFrame = options.requestFrame ?? requestAnimationFrame; + const cancelFrame = options.cancelFrame ?? cancelAnimationFrame; + const supportsPointerEvents = + options.supportsPointerEvents ?? typeof PointerEvent !== 'undefined'; + const eventName: PointerMoveEventName = supportsPointerEvents ? 'pointermove' : 'mousemove'; + const exitEventName: PointerExitEventName = supportsPointerEvents ? 'pointerout' : 'mouseout'; + const cancelEventName: PointerCancelEventName | null = supportsPointerEvents + ? 'pointercancel' + : null; + + let frame: number | null = null; + let pendingPosition: PhysicsPoint | null = null; + let disposed = false; + + const flush = () => { + frame = null; + if (disposed || !pendingPosition) return; + + options.updatePosition(pendingPosition); + pendingPosition = null; + }; + + const resetPosition = () => { + if (frame !== null) { + cancelFrame(frame); + frame = null; + } + pendingPosition = null; + options.updatePosition(getRangeCenter(options.range)); + }; + + const handleMove: EventListener = (event) => { + if (disposed) return; + + const pointerEvent = getLatestPointerEvent(event as unknown as PointerLikeEvent); + pendingPosition = mapClientPointToPhysics( + pointerEvent.clientX, + pointerEvent.clientY, + options.getBounds(), + options.range, + ); + + if (frame === null) { + frame = requestFrame(flush); + } + }; + + const handleExit: EventListener = (event) => { + if (disposed) return; + const exitEvent = event as unknown as PointerExitLikeEvent; + if (exitEvent.relatedTarget) return; + resetPosition(); + }; + + const handleBlur: EventListener = () => { + if (disposed) return; + resetPosition(); + }; + + options.target.addEventListener(eventName, handleMove, { passive: true }); + options.target.addEventListener(exitEventName, handleExit, { passive: true }); + if (cancelEventName) { + options.target.addEventListener(cancelEventName, handleBlur); + } + options.target.addEventListener('blur', handleBlur); + + return { + eventName, + exitEventName, + cancelEventName, + flush, + dispose() { + if (disposed) return; + disposed = true; + options.target.removeEventListener(eventName, handleMove); + options.target.removeEventListener(exitEventName, handleExit); + if (cancelEventName) { + options.target.removeEventListener(cancelEventName, handleBlur); + } + options.target.removeEventListener('blur', handleBlur); + if (frame !== null) { + cancelFrame(frame); + frame = null; + } + pendingPosition = null; + }, + }; +} diff --git a/src/motion/ScrollHandler.ts b/src/motion/ScrollHandler.ts index fb9c13b..3b3c389 100644 --- a/src/motion/ScrollHandler.ts +++ b/src/motion/ScrollHandler.ts @@ -1,11 +1,7 @@ - - - - - - export interface ScrollHandlerConfig { + /** Per-frame decay multiplier for scroll stickiness and velocity. Defaults to 0.92. */ decayRate?: number; + /** Maximum retained pull-force impulses. Defaults to 8, or 10 for explosive scrolls. Use 0 to keep scroll stickiness without retained pull forces. */ maxForces?: number; } @@ -23,13 +19,21 @@ export class ScrollHandler { private scrollDirection = 0; private pullForces: PullForce[] = []; private peakVelocity = 0; - private rafId: number | null = null; + private decayFrame: number | null = null; + private scrollEndTimer: ReturnType | null = null; + private disposed = false; + private maxForces: number | null = null; constructor(config?: ScrollHandlerConfig) { if (config?.decayRate) this.decayRate = config.decayRate; + if (typeof config?.maxForces === 'number') { + this.maxForces = Math.max(0, Math.floor(config.maxForces)); + } } public handleScroll(event: WheelEvent): void { + if (this.disposed) return; + const currentTime = Date.now(); const deltaTime = currentTime - this.lastScrollTime; @@ -68,9 +72,17 @@ export class ScrollHandler { this.lastScrollTime = currentTime; this.startDecay(); + this.scheduleScrollEnd(); + } + + private scheduleScrollEnd(): void { + if (this.scrollEndTimer !== null) { + clearTimeout(this.scrollEndTimer); + } - setTimeout(() => { - if (currentTime - this.lastScrollTime >= 200) { + this.scrollEndTimer = setTimeout(() => { + this.scrollEndTimer = null; + if (Date.now() - this.lastScrollTime >= 200) { this.isScrolling = false; this.totalScrollDistance = 0; this.peakVelocity = 0; @@ -82,7 +94,7 @@ export class ScrollHandler { speedStickiness: number, distanceStickiness: number, direction: number, - explosive: boolean + explosive: boolean, ): void { if (direction <= 0 || speedStickiness > 0.4 || distanceStickiness > 0.4 || explosive) { let pullStrength = speedStickiness + distanceStickiness * 0.7; @@ -101,24 +113,23 @@ export class ScrollHandler { strength: pullStrength, time: 0, randomness: randomnessFactor, - explosive: explosive, + explosive, }); - if (this.pullForces.length > (explosive ? 10 : 8)) { - this.pullForces.shift(); + const maxForces = this.maxForces ?? (explosive ? 10 : 8); + if (this.pullForces.length > maxForces) { + this.pullForces.splice(0, this.pullForces.length - maxForces); } } } private startDecay(): void { - // Cancel any in-flight decay so rapid handleScroll() calls don't - // queue overlapping RAF callbacks. - if (this.rafId !== null) { - cancelAnimationFrame(this.rafId); - this.rafId = null; - } + if (this.decayFrame !== null) return; const decay = () => { + this.decayFrame = null; + if (this.disposed) return; + this.stickiness *= this.decayRate; this.scrollVelocity *= this.decayRate; @@ -134,14 +145,13 @@ export class ScrollHandler { })); if (this.stickiness > 0.01 || this.pullForces.length > 0) { - this.rafId = requestAnimationFrame(decay); + this.decayFrame = requestAnimationFrame(decay); } else { this.stickiness = 0; this.scrollVelocity = 0; - this.rafId = null; } }; - this.rafId = requestAnimationFrame(decay); + this.decayFrame = requestAnimationFrame(decay); } public getStickiness(): number { @@ -171,4 +181,25 @@ export class ScrollHandler { public getPeakVelocity(): number { return this.peakVelocity; } + + public dispose(): void { + this.disposed = true; + + if (this.decayFrame !== null) { + cancelAnimationFrame(this.decayFrame); + this.decayFrame = null; + } + + if (this.scrollEndTimer !== null) { + clearTimeout(this.scrollEndTimer); + this.scrollEndTimer = null; + } + + this.stickiness = 0; + this.scrollVelocity = 0; + this.totalScrollDistance = 0; + this.peakVelocity = 0; + this.isScrolling = false; + this.pullForces = []; + } } diff --git a/src/motion/index.ts b/src/motion/index.ts index 5ded2ef..1e26e10 100644 --- a/src/motion/index.ts +++ b/src/motion/index.ts @@ -2,5 +2,32 @@ -export { DeviceMotion, type DeviceMotionCallback } from './DeviceMotion.js'; +export { + DeviceMotion, + type DeviceMotionCallback, + type DeviceMotionOptions, + type DeviceMotionPermissionState, + type MotionVector, +} from './DeviceMotion.js'; +export { + mapClientPointToPhysics, + type PhysicsPoint, + type PhysicsRange, + type PointerBounds, +} from './PointerMapper.js'; +export { + createPointerPhysicsController, + detectPointerPhysicsCapability, + getLatestPointerEvent, + type PointerCapabilityEnvironment, + type PointerCancelEventName, + type PointerExitEventName, + type PointerExitLikeEvent, + type PointerLifecycleEventName, + type PointerLikeEvent, + type PointerMoveEventName, + type PointerPhysicsController, + type PointerPhysicsControllerOptions, + type PointerPhysicsEventTarget, +} from './PointerPhysicsController.js'; export { ScrollHandler, type ScrollHandlerConfig, type PullForce } from './ScrollHandler.js'; diff --git a/src/svelte/BlobSVG.svelte.d.ts b/src/svelte/BlobSVG.svelte.d.ts new file mode 100644 index 0000000..4e9ee49 --- /dev/null +++ b/src/svelte/BlobSVG.svelte.d.ts @@ -0,0 +1,11 @@ +import type { Component } from 'svelte'; +import type { BlobPhysics } from '../core/BlobPhysics.js'; +import type { ConvexBlob } from '../core/types.js'; + +export interface BlobSVGProps { + blobs?: ConvexBlob[]; + physics?: BlobPhysics | null; +} + +declare const BlobSVG: Component; +export default BlobSVG; diff --git a/src/svelte/TinyVectors.svelte b/src/svelte/TinyVectors.svelte index c480f5f..8a70989 100644 --- a/src/svelte/TinyVectors.svelte +++ b/src/svelte/TinyVectors.svelte @@ -2,12 +2,24 @@ import { browser } from '../core/browser.js'; import { untrack } from 'svelte'; import { BlobPhysics, type BlobPhysicsConfig } from '../core/BlobPhysics.js'; - import { DeviceMotion } from '../motion/DeviceMotion.js'; + import { + DeviceMotion, + getDeviceMotionCapabilityState, + isDeviceMotionPermissionRequired, + type MotionVector, + } from '../motion/DeviceMotion.js'; + import { + createPointerPhysicsController, + detectPointerPhysicsCapability, + type PointerPhysicsController, + } from '../motion/PointerPhysicsController.js'; + import type { PointerBounds } from '../motion/PointerMapper.js'; import { ScrollHandler } from '../motion/ScrollHandler.js'; - import { THEME_PRESETS, type ThemePresetName } from '../core/schema.js'; + import { THEME_PRESET_COLORS } from '../core/theme-colors.js'; + import type { ThemePresetName } from '../core/theme-presets.js'; + import type { TinyVectorsDeviceMotionStatus } from './types.js'; import BlobSVG from './BlobSVG.svelte'; - // Props interface Props { /** Theme preset name */ theme?: ThemePresetName; @@ -23,10 +35,20 @@ blobCount?: number; /** Physics configuration */ physicsConfig?: Partial; - /** Enable device motion (accelerometer) */ + /** Enable device orientation based motion */ enableDeviceMotion?: boolean; /** Enable scroll physics */ enableScrollPhysics?: boolean; + /** Enable pointer/mouse physics */ + enablePointerPhysics?: boolean; + /** Scales normalized screen-aligned tilt vectors before applying them to physics. */ + deviceMotionStrength?: number; + /** Samples used by calibrateDeviceMotion() when no explicit count is supplied. */ + deviceMotionCalibrationSamples?: number; + /** Milliseconds before paused device-orientation IO resets to neutral. */ + deviceMotionIdleResetMs?: number; + /** Optional diagnostics hook for browser/dev harnesses. */ + onDeviceMotion?: (motionData: MotionVector) => void; } let { @@ -39,101 +61,79 @@ physicsConfig = {}, enableDeviceMotion = true, enableScrollPhysics = true, + enablePointerPhysics = true, + deviceMotionStrength = 0.8, + deviceMotionCalibrationSamples = 8, + deviceMotionIdleResetMs = 2000, + onDeviceMotion, }: Props = $props(); - // State - use regular variables for non-reactive state + let containerElement: HTMLDivElement | undefined = $state(undefined); let blobs = $state>([]); let isReady = $state(false); - let isMobileDevice = $state(false); - let hasAccelerometerAccess = $state(false); - // Internal handles that are passed into child components need to stay reactive. let physics = $state(null); let animationFrame: number | null = null; let lastTime = 0; let deviceMotion: DeviceMotion | null = null; let scrollHandler: ScrollHandler | null = null; - let gravityX = 0; - let gravityY = 0; - let tiltX = 0; - let tiltY = 0; - let tiltZ = 0; + let pointerController: PointerPhysicsController | null = null; - // Get theme colors - use $derived.by for computed values const themeColors = $derived.by(() => { if (colors.length > 0) return colors; - const preset = THEME_PRESETS[theme]; - if (!preset || !preset.hasVectors) return []; - return preset.colors.map((c) => c.color); + return THEME_PRESET_COLORS[theme] ?? []; }); - // Default physics config - const defaultPhysicsConfig: BlobPhysicsConfig = { - antiClusteringStrength: 0.15, - bounceDamping: 0.7, - deformationSpeed: 0.5, - territoryStrength: 0.1, - viscosity: 0.3, - useSpatialHash: true, - useGaussianSmoothing: true, - useSpringSystem: true, - springConfig: {}, + const detectDeviceMotionCapability = (): boolean => { + return browser && getDeviceMotionCapabilityState() === 'unknown'; }; - // Detect mobile device - const detectMobileDevice = (): boolean => { - if (!browser) return false; - const userAgent = navigator.userAgent.toLowerCase(); - const mobileKeywords = ['mobile', 'android', 'iphone', 'ipad', 'ipod', 'blackberry', 'windows phone']; - const isMobileUserAgent = mobileKeywords.some((keyword) => userAgent.includes(keyword)); - const isMobileScreen = window.innerWidth <= 768 || window.innerHeight <= 768; - const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0; - const hasOrientationAPI = 'DeviceOrientationEvent' in window; - return (isMobileUserAgent || (isMobileScreen && hasTouchScreen)) && hasOrientationAPI; - }; + const createDeviceMotion = (): DeviceMotion => + new DeviceMotion(handleDeviceMotion, { + calibrationSamples: deviceMotionCalibrationSamples, + deadZone: 0.015, + idleResetMs: deviceMotionIdleResetMs, + }); - // DeviceMotion now emits already-filtered, axis-remapped, screen-aligned - // tilt vectors in [-1, 1] (One-Euro internally, slow baseline subtraction, - // face-down suppression, screen.orientation remap). Pass through directly - // — no extra EMA, no axis swap, no negation. The 0.8 magnitude scaler is - // preserved so gravity strength matches the previous code's feel at the - // physics layer. - // - // Y-gravity sign note: the previous handler computed gravityY = -beta, - // which made forward-tilt drive gravity UP the screen (away from the - // viewer). That was counter-intuitive — forward tilt should pull stuff - // toward the viewer (positive screen-Y, downward). TiltSource emits - // screen-aligned values directly, so we use motionData.y unchanged. - // This is the intentional fix that the canonical consumer was working - // around by setting enableDeviceMotion={false}. - const handleDeviceMotion = (motionData: { x: number; y: number; z: number }) => { - if (!hasAccelerometerAccess || !physics) return; - tiltX = motionData.x; - tiltY = motionData.y; - tiltZ = motionData.z; - gravityX = motionData.x * 0.8; - gravityY = motionData.y * 0.8; - physics.setGravity({ x: gravityX, y: gravityY }); - physics.setTilt({ x: tiltX, y: tiltY, z: tiltZ }); - }; + const handleDeviceMotion = (motionData: MotionVector) => { + if (!physics) return; - // Request accelerometer permission - const requestAccelerometerPermission = async (): Promise => { - if (!isMobileDevice || !deviceMotion) return; + onDeviceMotion?.(motionData); - try { - const hasPermission = await deviceMotion.requestPermission(); - hasAccelerometerAccess = hasPermission; - if (hasPermission) { - console.log('[TinyVectors] Accelerometer access granted'); - } - } catch (error) { - console.log('[TinyVectors] Could not request accelerometer permission:', error); - hasAccelerometerAccess = false; - } + // DeviceMotion emits screen-aligned tilt, so no old beta/gamma axis swap. + physics.setGravity({ + x: motionData.x * deviceMotionStrength, + y: motionData.y * deviceMotionStrength, + }); + physics.setTilt(motionData); }; - // Handle scroll passively — never block native scrolling + export async function requestDeviceMotionPermission(): Promise { + if (!browser || !enableDeviceMotion || !physics || !detectDeviceMotionCapability()) return false; + + deviceMotion ??= createDeviceMotion(); + + const hasPermission = await deviceMotion.requestPermission(); + return hasPermission; + } + + export function calibrateDeviceMotion(samples?: number): void { + deviceMotion?.calibrate(samples); + } + + export function getDeviceMotionStatus(): TinyVectorsDeviceMotionStatus { + const capabilityState = getDeviceMotionCapabilityState(); + const permissionState = deviceMotion?.getPermissionState() ?? capabilityState; + + return { + enabled: enableDeviceMotion, + supported: capabilityState !== 'unsupported' && capabilityState !== 'insecure', + requiresPermission: browser && isDeviceMotionPermissionRequired(), + permissionState, + active: deviceMotion?.isActive() ?? false, + }; + } + const handleScroll = (event: WheelEvent) => { if (!scrollHandler || !physics) return; scrollHandler.handleScroll(event); @@ -141,14 +141,32 @@ physics.setScrollStickiness(stickiness); }; - // Animation tick function - updates blobs state once per frame + const getPointerBounds = (): PointerBounds => { + const rect = containerElement?.getBoundingClientRect(); + + if (rect && rect.width > 0 && rect.height > 0) { + return { + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + }; + } + + return { + left: 0, + top: 0, + width: window.innerWidth || 1, + height: window.innerHeight || 1, + }; + }; + function tick(currentTime: number) { const dt = Math.min((currentTime - lastTime) / 1000, 0.033); lastTime = currentTime; if (physics) { physics.tick(dt, currentTime / 1000); - // Update blobs state - this triggers re-render blobs = physics.getBlobs(themeColors); } @@ -168,34 +186,37 @@ } } - // Single initialization effect $effect(() => { if (!browser || !shouldLoad) return; - // Use untrack to prevent this effect from re-running on state changes + let disposed = false; + const deviceMotionEnabled = enableDeviceMotion; + const scrollPhysicsEnabled = enableScrollPhysics; + const pointerPhysicsEnabled = enablePointerPhysics; + let wheelListenerAttached = false; + untrack(() => { - const config = { ...defaultPhysicsConfig, ...physicsConfig }; - physics = new BlobPhysics(blobCount, config); + // BlobPhysics owns base defaults; this component forwards caller overrides. + const currentPhysics = new BlobPhysics(blobCount, physicsConfig); + physics = currentPhysics; + + currentPhysics.init().then(() => { + if (disposed || physics !== currentPhysics) return; - physics.init().then(() => { - // Detect mobile - isMobileDevice = detectMobileDevice(); + const hasDeviceMotionCapability = detectDeviceMotionCapability(); isReady = true; - // Initialize device motion on mobile - if (enableDeviceMotion && isMobileDevice) { - deviceMotion = new DeviceMotion(handleDeviceMotion); - deviceMotion.initialize().then(() => { - setTimeout(requestAccelerometerPermission, 1000); - }); + if (deviceMotionEnabled && hasDeviceMotionCapability) { + if (!deviceMotion) { + deviceMotion = createDeviceMotion(); + void deviceMotion.initialize(); + } } - // Initialize scroll handler - if (enableScrollPhysics) { + if (scrollPhysicsEnabled) { scrollHandler = new ScrollHandler(); } - // Start animation if enabled if (animated) { startAnimation(); } else if (physics) { @@ -203,24 +224,45 @@ } }); - // Set up scroll listener - if (enableScrollPhysics) { + if (scrollPhysicsEnabled) { window.addEventListener('wheel', handleScroll, { passive: true }); + wheelListenerAttached = true; + } + + if (pointerPhysicsEnabled) { + const hasPointerCapability = detectPointerPhysicsCapability(window); + if (hasPointerCapability) { + pointerController = createPointerPhysicsController({ + target: window, + getBounds: getPointerBounds, + supportsPointerEvents: 'PointerEvent' in window, + updatePosition(position) { + physics?.updateMousePosition(position.x, position.y); + }, + }); + } } }); return () => { + disposed = true; stopAnimation(); - if (enableScrollPhysics && browser) { + if (wheelListenerAttached) { window.removeEventListener('wheel', handleScroll); } + pointerController?.dispose(); + pointerController = null; deviceMotion?.cleanup(); + deviceMotion = null; + scrollHandler?.dispose(); + scrollHandler = null; physics?.dispose(); physics = null; + isReady = false; + blobs = []; }; }); - // Handle animated prop changes $effect(() => { if (!isReady) return; @@ -234,7 +276,13 @@ {#if shouldLoad && themeColors.length > 0}