diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..60990a06 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# standardrb reformat from the 1.4 -> 1.56 upgrade +5fe4fe5188f75461be817528c8d911090ce94340 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7ecea79..ce8d4225 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [main, 1.x, beta] pull_request: {} workflow_dispatch: {} @@ -15,145 +15,62 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: - ruby-version: "3.0" + ruby-version: "3.2" bundler-cache: true - name: Run standardrb - run: bundle exec standardrb --no-fix --format progress + run: | + bundle exec standardrb --no-fix --format progress || { + echo "::error::Lint failed. Almost everything standard flags is auto-fixable — run 'bundle exec standardrb --fix' locally and push the result." + exit 1 + } test: - name: "Tests - ${{ matrix.ruby }} | ${{ matrix.gemfile }} | Appraisal: ${{ matrix.appraisal }}" + name: "Tests - ${{ matrix.ruby }} | ${{ matrix.gemfile }}" runs-on: ubuntu-latest strategy: fail-fast: false + # Every cell is listed explicitly rather than built from a cross product + # plus `exclude:`. An exclude row naming a gemfile that does not exist is + # silently ignored, which is how this matrix carried four dead rows for a + # "rails_7_0_graphiti_rails.gemfile" that was never generated. A bad + # gemfile path in an include row fails the job loudly instead. + # + # Ruby coverage per Rails version tracks that version's own supported + # range; see Appraisals for the matching sqlite3 constraints. matrix: - ruby: - - "2.7" - - "3.0" - - "3.1" - - "3.2" - - "3.3" - - "3.4" - gemfile: - - Gemfile - - gemfiles/rails_5_2.gemfile - - gemfiles/rails_6.gemfile - - gemfiles/rails_7.gemfile - - gemfiles/rails_5_2_graphiti_rails.gemfile - - gemfiles/rails_6_graphiti_rails.gemfile - - gemfiles/rails_7_graphiti_rails.gemfile - - gemfiles/rails_7_1_graphiti_rails.gemfile - - gemfiles/rails_7_2_graphiti_rails.gemfile - - gemfiles/rails_8_0_graphiti_rails.gemfile - - gemfiles/rails_8_1_graphiti_rails.gemfile - appraisal: - - true - - false include: - - ruby: ruby-head - gemfile: gemfiles/rails_7_1.gemfile - appraisal: true - - ruby: ruby-head - gemfile: gemfiles/rails_7_2_graphiti_rails.gemfile - appraisal: true - - ruby: ruby-head - gemfile: gemfiles/rails_8_0_graphiti_rails.gemfile - appraisal: true - - ruby: ruby-head - gemfile: gemfiles/rails_8_1_graphiti_rails.gemfile - appraisal: true - - ruby: ruby-head - gemfile: Gemfile - appraisal: false - exclude: - # Skip some extra variants - - gemfile: Gemfile - appraisal: true - - gemfile: gemfiles/rails_5_2.gemfile - appraisal: false - - gemfile: gemfiles/rails_6.gemfile - appraisal: false - - gemfile: gemfiles/rails_7.gemfile - appraisal: false - - gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile - appraisal: false - - gemfile: gemfiles/rails_6_graphiti_rails.gemfile - appraisal: false - - gemfile: gemfiles/rails_7_graphiti_rails.gemfile - appraisal: false - - gemfile: gemfiles/rails_7_1_graphiti_rails.gemfile - appraisal: false - - gemfile: gemfiles/rails_7_2_graphiti_rails.gemfile - appraisal: false - - gemfile: gemfiles/rails_8_0_graphiti_rails.gemfile - appraisal: false - - gemfile: gemfiles/rails_8_1_graphiti_rails.gemfile - appraisal: false + # No Rails - the library on its own, across every supported Ruby + - { ruby: "3.2", gemfile: Gemfile, appraisal: false } + - { ruby: "3.3", gemfile: Gemfile, appraisal: false } + - { ruby: "3.4", gemfile: Gemfile, appraisal: false } + - { ruby: "4.0", gemfile: Gemfile, appraisal: false } + - { ruby: ruby-head, gemfile: Gemfile, appraisal: false } - # Rails 8.1 needs ruby 3.2 + - - gemfile: gemfiles/rails_8_1_graphiti_rails.gemfile - ruby: 2.7 - - gemfile: gemfiles/rails_8_1_graphiti_rails.gemfile - ruby: 3.0 - - gemfile: gemfiles/rails_8_1_graphiti_rails.gemfile - ruby: 3.1 + # Rails 7.1 - the floor + - { ruby: "3.2", gemfile: gemfiles/rails_7_1.gemfile, appraisal: true } + - { ruby: "3.4", gemfile: gemfiles/rails_7_1.gemfile, appraisal: true } + - { ruby: "4.0", gemfile: gemfiles/rails_7_1.gemfile, appraisal: true } - # Rails 8 needs ruby 3.2 + - - gemfile: gemfiles/rails_8_0_graphiti_rails.gemfile - ruby: 2.7 - - gemfile: gemfiles/rails_8_0_graphiti_rails.gemfile - ruby: 3.0 - - gemfile: gemfiles/rails_8_0_graphiti_rails.gemfile - ruby: 3.1 + # Rails 7.2 + - { ruby: "3.2", gemfile: gemfiles/rails_7_2.gemfile, appraisal: true } + - { ruby: "3.4", gemfile: gemfiles/rails_7_2.gemfile, appraisal: true } + - { ruby: "4.0", gemfile: gemfiles/rails_7_2.gemfile, appraisal: true } + - { ruby: ruby-head, gemfile: gemfiles/rails_7_2.gemfile, appraisal: true } - # Rails 7.2 needs ruby 3.1 + - - gemfile: gemfiles/rails_7_2_graphiti_rails.gemfile - ruby: 2.7 - - gemfile: gemfiles/rails_7_2_graphiti_rails.gemfile - ruby: 3.0 + # Rails 8.0 + - { ruby: "3.2", gemfile: gemfiles/rails_8_0.gemfile, appraisal: true } + - { ruby: "3.4", gemfile: gemfiles/rails_8_0.gemfile, appraisal: true } + - { ruby: "4.0", gemfile: gemfiles/rails_8_0.gemfile, appraisal: true } + - { ruby: ruby-head, gemfile: gemfiles/rails_8_0.gemfile, appraisal: true } - # Rails 7.1 needs ruby 3.0 + - - gemfile: gemfiles/rails_7_1_graphiti_rails.gemfile - ruby: 2.7 - - gemfile: gemfiles/rails_7_1_graphiti_rails.gemfile - ruby: 3.0 + # Rails 8.1 + - { ruby: "3.2", gemfile: gemfiles/rails_8_1.gemfile, appraisal: true } + - { ruby: "3.4", gemfile: gemfiles/rails_8_1.gemfile, appraisal: true } + - { ruby: "4.0", gemfile: gemfiles/rails_8_1.gemfile, appraisal: true } + - { ruby: ruby-head, gemfile: gemfiles/rails_8_1.gemfile, appraisal: true } - # Rails 7.0 needs ruby 3.1 + - - gemfile: gemfiles/rails_7_0_graphiti_rails.gemfile - ruby: 3.2 - - gemfile: gemfiles/rails_7_0_graphiti_rails.gemfile - ruby: 3.3 - - gemfile: gemfiles/rails_7_0_graphiti_rails.gemfile - ruby: 3.4 - - # Rails 6 needs < ruby 3.4 - - gemfile: gemfiles/rails_6.gemfile - ruby: 3.4 - - gemfile: gemfiles/rails_6_graphiti_rails.gemfile - ruby: 3.4 - - # Rails 5 can't run on Ruby 3 - - gemfile: gemfiles/rails_5_2.gemfile - ruby: 3.0 - - gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile - ruby: 3.0 - - gemfile: gemfiles/rails_5_2.gemfile - ruby: 3.1 - - gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile - ruby: 3.1 - - gemfile: gemfiles/rails_5_2.gemfile - ruby: 3.2 - - gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile - ruby: 3.2 - - gemfile: gemfiles/rails_5_2.gemfile - ruby: 3.3 - - gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile - ruby: 3.3 - - gemfile: gemfiles/rails_5_2.gemfile - ruby: 3.4 - - gemfile: gemfiles/rails_5_2_graphiti_rails.gemfile - ruby: 3.4 continue-on-error: ${{ matrix.ruby == 'ruby-head' }} env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }} @@ -161,21 +78,80 @@ jobs: - name: Set up Appraisal if: matrix.appraisal run: echo "APPRAISAL_INITIALIZED=true" >> $GITHUB_ENV - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Run tests run: bundle exec rspec + # spec_helper narrows "bundle exec rspec" to spec/integration/rails when + # APPRAISAL_INITIALIZED is set, so the step above never runs the library + # suite. This is the only cell that runs it against a pinned Rails. + - name: Run the library suite against this Rails + if: matrix.appraisal + run: env -u APPRAISAL_INITIALIZED bundle exec rspec + example-app: + name: "Example app (employee_directory)" + runs-on: ubuntu-latest + # The example app lives in its own repo and can lag behind; it is an + # integration canary, not a merge gate. + continue-on-error: true + env: + GRAPHITI_PATH: ${{ github.workspace }}/graphiti + BUNDLE_FROZEN: "false" # the path override rewrites the checked-in lockfile + defaults: + run: + working-directory: employee_directory + steps: + - uses: actions/checkout@v4 + with: + path: graphiti + - uses: actions/checkout@v4 + with: + repository: graphiti-api/employee_directory + path: employee_directory + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + - name: Bundle against this graphiti + run: bundle install + - name: Run the example app suite + run: | + bundle exec rails db:test:prepare + bundle exec rspec + examples: + name: "Examples (plain ruby, sinatra)" + runs-on: ubuntu-latest + # In-tree examples run against the working copy via their path: Gemfiles. + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + - name: Run the plain ruby example + working-directory: examples/plain_ruby + run: | + bundle install + bundle exec ruby index.rb + - name: Run the sinatra example + working-directory: examples/sinatra + run: | + bundle install + bundle exec ruby smoke.rb publish: name: Release runs-on: ubuntu-latest + # Only main auto-releases. Beta releases are cut by manually running + # "Generate New Release" with the beta branch selected. if: github.ref == 'refs/heads/main' - needs: [test] + needs: [lint, test] steps: - name: Dispatch Release uses: benc-uk/workflow-dispatch@v1 with: workflow: Generate New Release + # Without an explicit ref the dispatch runs against the default + # branch rather than the branch that triggered it. + ref: ${{ github.ref }} token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..1149054e --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,60 @@ +name: Docs + +# Publishes the documentation site from beta, and from main once 2.0 lands +# there. graphiti.dev now points at this repo via website/static/CNAME, so this +# deploy is the live site: the 1.x Jekyll site is frozen under +# website/static/1.13 and ships with every build, and graphiti-api.github.io no +# longer serves the domain. +on: + push: + branches: [beta, main] + paths: + - 'docs/**' + - 'website/**' + - '.github/workflows/docs.yml' + workflow_dispatch: {} + +# One static group: deploys from different branches force-push the same +# gh-pages branch, so they must not interleave. +concurrency: + group: docs + cancel-in-progress: true + +permissions: + contents: write + +jobs: + deploy: + name: Build and publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + cache-dependency-path: website/package-lock.json + + - name: Install + working-directory: website + run: npm ci + + - name: Build + working-directory: website + run: npm run build + + - name: Publish to gh-pages + working-directory: website/build + run: | + set -euo pipefail + # Pages skips directories beginning with an underscore unless Jekyll + # is switched off, and Docusaurus emits some. + touch .nojekyll + git init --quiet --initial-branch=gh-pages + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add --all + git commit --quiet -m "docs: build ${GITHUB_SHA::7} from ${GITHUB_REF_NAME}" + git push --force --quiet \ + "https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git" \ + gh-pages diff --git a/.github/workflows/notify-example-app.yml b/.github/workflows/notify-example-app.yml new file mode 100644 index 00000000..c2d0b6bc --- /dev/null +++ b/.github/workflows/notify-example-app.yml @@ -0,0 +1,18 @@ +name: Notify example app + +on: + release: + types: [published] + +jobs: + dispatch: + name: Dispatch employee_directory update + runs-on: ubuntu-latest + steps: + - name: Send repository_dispatch + run: | + gh api -X POST repos/graphiti-api/employee_directory/dispatches \ + -f event_type=graphiti-release \ + -f 'client_payload[version]=${{ github.event.release.tag_name }}' + env: + GH_TOKEN: ${{ secrets.EXAMPLE_APP_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 944ffe9e..d75da6e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,27 +10,29 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v5 with: - submodules: true persist-credentials: false + # semantic-release reads the tag history to work out the next version + fetch-depth: 0 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "3.1" + ruby-version: "4.0" - name: Build run: | gem install bundler - git submodule update --init --recursive bundle install --jobs 4 --retry 3 - name: Setup Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: - node-version: 14 + # semantic-release 25 and its plugins require ^22.22.2 || >=24.15 + node-version: "24" + cache: npm - name: Install Dependencies - run: yarn install --frozen-lockfile + run: npm ci - name: Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GEM_HOST_API_KEY: ${{ secrets.GEM_HOST_API_KEY }} - run: node_modules/.bin/semantic-release + run: npx semantic-release diff --git a/.gitignore b/.gitignore index eaf46d2d..de139bd2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,12 @@ spec/.rspec-examples spec/dummy/log/* *~ .vscode/ + +# Docusaurus +website/build/ +website/.docusaurus/ + +.DS_Store +.idea/ +examples/*/Gemfile.lock +examples/sinatra/*.sqlite3* diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..64a57d97 --- /dev/null +++ b/.npmrc @@ -0,0 +1,9 @@ +# semantic-release-rubygem@1.2.0 declares `peerDependencies: { semantic-release: +# ">=16.0.0 <18.0.0" }`, but it was last published in July 2020 and that range has +# simply not been updated since. This project has been releasing against +# semantic-release 19 for years, and the plugin loads cleanly under 25 exposing +# its verifyConditions/prepare/publish hooks - it only touches `env` and `cwd` on +# the plugin context, both stable across semantic-release majors. +# +# Without this, `npm ci` fails the install with ERESOLVE. +legacy-peer-deps=true diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..eb39e538 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.3 diff --git a/.standard.yml b/.standard.yml index 39471890..5c66111e 100644 --- a/.standard.yml +++ b/.standard.yml @@ -1,9 +1,18 @@ fix: true parallel: true -ruby_version: 2.7 +ruby_version: 3.2 ignore: - 'vendor/**/*' + # The Rails app template's heredocs are written into the user's files, so + # their literal indentation is load-bearing and the <<~ autofix would + # change what gets generated. + - 'templates/**/*': + - Layout/HeredocIndentation + - Layout/ClosingHeredocIndentation + # The docs site, including the frozen 1.x archive under static/. None of it + # is ours to restyle, and some of it is generated. + - 'website/**/*' - 'spec/**/*': # There are some false hits when doing repeated # Class.new blocks in different test cases diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..05913eae --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +ruby 4.0.6 diff --git a/Appraisals b/Appraisals index 4de03ce0..aed9dbe3 100644 --- a/Appraisals +++ b/Appraisals @@ -1,60 +1,46 @@ -appraise "rails-5_2" do - gem "rails", "~> 5.2.0" - gem "rspec-rails" - gem "sqlite3", "~> 1.4.0" - gem "database_cleaner" -end - -appraise "rails-5_2-graphiti-rails" do - gem "rails", "~> 5.2.0" - gem "rspec-rails" - gem "sqlite3", "~> 1.4.0" - gem "database_cleaner" - gem "graphiti-rails", "~> 0.4.0" -end - -appraise "rails-6" do - gem "rails", "~> 6.0" - gem "rspec-rails" - gem "sqlite3", "~> 1.4.0" - gem "database_cleaner" -end +# Graphiti's Rails integration ships in the gem itself as of 2.0, so there is +# one appraisal per Rails version rather than a with/without graphiti-rails pair. +# +# Rails 7.1 is the floor: 2.0 requires Ruby >= 3.2, and Rails 6.1 and 7.0 do not +# support it. Apps on older Rails stay on the graphiti 1.x branch. +# +# Every Rails requirement pins the minor ("~> 7.1.0", not "~> 7.1"). The looser +# form lets a newer minor satisfy it, which is how rails-7-1 and rails-8-0 ended +# up resolving to 7.2 and 8.1 - two appraisals apiece testing the same Rails and +# neither testing the version it was named for. +# +# sqlite3 pins follow what each Rails version's adapter demands at require time +# (activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb): +# Rails 7.1 -> gem "sqlite3", "~> 1.4" (2.x raises Gem::LoadError) +# Rails 7.2 -> gem "sqlite3", ">= 1.4" +# Rails 8.0 / 8.1 -> gem "sqlite3", ">= 2.1" +# "~> 1.4" rather than "~> 1.4.0" is deliberate: it resolves to 1.7.x, which +# still builds on modern Rubies, where 1.4.x no longer does. -appraise "rails-6-graphiti-rails" do - gem "rails", "~> 6.0" - gem "rspec-rails" - gem "sqlite3", "~> 1.4.0" - gem "database_cleaner" - gem "graphiti-rails", "~> 0.4.0" -end - -appraise "rails-7" do - gem "rails", "~> 7.0" +appraise "rails-7-1" do + gem "rails", "~> 7.1.0" gem "rspec-rails" - gem "sqlite3", "~> 1.4.0" - gem "database_cleaner" + gem "responders" + gem "sqlite3", "~> 1.4" end -appraise "rails-7-graphiti-rails" do - gem "rails", "~> 7.0" +appraise "rails-7-2" do + gem "rails", "~> 7.2.0" gem "rspec-rails" - gem "sqlite3", "~> 1.4.0" - gem "database_cleaner" - gem "graphiti-rails", "~> 0.4.0" + gem "responders" + gem "sqlite3", "~> 2.1" end -appraise "rails-7-1" do - gem "rails", "~> 7.1" +appraise "rails-8-0" do + gem "rails", "~> 8.0.0" gem "rspec-rails" - # This appraisal is the ruby-head job; sqlite3 1.4.x cannot build on Ruby 4.x + gem "responders" gem "sqlite3", "~> 2.1" - gem "database_cleaner" end -appraise "rails-7-1-graphiti-rails" do - gem "rails", "~> 7.1" +appraise "rails-8-1" do + gem "rails", "~> 8.1.0" gem "rspec-rails" - gem "sqlite3", "~> 1.4.0" - gem "database_cleaner" - gem "graphiti-rails", "~> 0.4.0" + gem "responders" + gem "sqlite3", "~> 2.1" end diff --git a/CHANGELOG.md b/CHANGELOG.md index b2ffff6d..ef16aa25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,233 @@ graphiti changelog +# [2.0.0-beta.13](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.12...v2.0.0-beta.13) (2026-08-31) + + +### Bug Fixes + +* compare the endpoint id to the payload id as strings ([62051d7](https://github.com/graphiti-api/graphiti/commit/62051d7531432357db7bc464cce77ccc55168d07)) +* evaluate writable guards under the action being performed ([d56b86a](https://github.com/graphiti-api/graphiti/commit/d56b86a5ffaac4106aaea1e2ee5400069a4205fe)) +* **generators:** stop injecting a routes host into config/application.rb ([db92d06](https://github.com/graphiti-api/graphiti/commit/db92d0649ca0246662a86b1172229eaa2a817d2b)) +* keep a belongs_to linkage when another include shares its name ([2c9c7c7](https://github.com/graphiti-api/graphiti/commit/2c9c7c7ac06cd319ac95e1445eb55a0322841de2)) +* keep deep filters narrow when an include path repeats ([833a10b](https://github.com/graphiti-api/graphiti/commit/833a10be120a4f2cd615ad05f750c7f246d62801)) +* keep request state in fiber storage so it survives into child fibers ([6569935](https://github.com/graphiti-api/graphiti/commit/6569935c2140d4dd91cd6d4a7f9ec7c1bd05c888)) +* only register the jsonapi mime type when it is missing ([88401ab](https://github.com/graphiti-api/graphiti/commit/88401abc8c77f1e859b4b8da3f9574c75834eb43)) +* with_options accepts the predicate backed relationship options ([71ab441](https://github.com/graphiti-api/graphiti/commit/71ab4418bccc4cff7e6cc10151c1728641d25d34)) + + +### Features + +* write every error code Graphiti renders into the generated locale file ([15f19c1](https://github.com/graphiti-api/graphiti/commit/15f19c1dbe56559d9d041fa50e2cc5ea7b2dcf4b)) + + +### Performance Improvements + +* record latest performance stats ([7380ef9](https://github.com/graphiti-api/graphiti/commit/7380ef912e9f3b99618bf47ed04e06d1efad4e6c)) + +# [2.0.0-beta.12](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.11...v2.0.0-beta.12) (2026-08-30) + + +### Bug Fixes + +* compare each concurrency path against itself in the table to better illustrate the performance wins ([d0f0e92](https://github.com/graphiti-api/graphiti/commit/d0f0e92860b3b4affe3fca6dbb9b871fe8a1374f)) + + +### Features + +* add schema rake tasks, so checking a schema no longer means running the suite ([6f4783d](https://github.com/graphiti-api/graphiti/commit/6f4783d5654659125b7eb358bf1175b6feaacb7c)) +* declare Graphiti's client errors in rescue_responses ([e369824](https://github.com/graphiti-api/graphiti/commit/e369824d6e11b671ffd212caad3d0a43a7ceefcf)) +* mark deprecated settings in the generated ApplicationResource ([52b0642](https://github.com/graphiti-api/graphiti/commit/52b0642e689a16d14dacaf4e28a2641f10774cf0)) +* take error text from locale keys ([d01fd06](https://github.com/graphiti-api/graphiti/commit/d01fd06fc1ede8c3dd798ec48de4d5b01645d87e)), closes [#216](https://github.com/graphiti-api/graphiti/issues/216) + +# [2.0.0-beta.11](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.10...v2.0.0-beta.11) (2026-08-27) + + +### Bug Fixes + +* always render included when the client asked to include ([8691b3f](https://github.com/graphiti-api/graphiti/commit/8691b3fd08f3cae0481ba72839911fdc71ef5634)) +* autolink = true no longer clobbers an inherited :on_demand ([2d26f8c](https://github.com/graphiti-api/graphiti/commit/2d26f8cb567e82a16918345de9114b8ceef64523)) +* read the links param from the query, not the context object ([3b934cb](https://github.com/graphiti-api/graphiti/commit/3b934cb25be049e1e80ba400123da944cfd23d04)) +* stop assigning the same child twice when a resource repeats in the include path ([588503b](https://github.com/graphiti-api/graphiti/commit/588503b109508495d8170373d80019c6971f7d9c)) + + +### Features + +* omit relationships that render no ids and no link ([916aa8c](https://github.com/graphiti-api/graphiti/commit/916aa8c9c596c5d9b6c802c5ab13b766a13d1e70)) + + +### Performance Improvements + +* memoize whether a belongs_to renders ids from its foreign key ([dcb60b2](https://github.com/graphiti-api/graphiti/commit/dcb60b2096c414756ccff534773d2e45594f0144)) +* only deduplicate resources that two include paths can both reach ([744f014](https://github.com/graphiti-api/graphiti/commit/744f014249382bc25d5ea43f692778ad7adc25c3)) +* resolve a lone sideload inline rather than through the pool ([3415592](https://github.com/graphiti-api/graphiti/commit/3415592d75900389a8f11f53b65ab399e297e4a9)) +* reuse the resolved data and proxies when building cache keys ([8a17350](https://github.com/graphiti-api/graphiti/commit/8a17350542ba0062fdcddd115afe8041b62198c7)) +* skip the on-demand link strip when no relationship renders links on demand ([cf86a8c](https://github.com/graphiti-api/graphiti/commit/cf86a8c249077d9f9d5a13de49989d102bbaed04)) + +# [2.0.0-beta.10](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.9...v2.0.0-beta.10) (2026-08-24) + + +### Bug Fixes + +* apply sideloads at definition once setup! has run ([39ef393](https://github.com/graphiti-api/graphiti/commit/39ef39383520816319282854c0ae99677bcd5127)), closes [#524](https://github.com/graphiti-api/graphiti/issues/524) +* give client-caused errors real detail and stop 500ing on bad query params ([35acc3e](https://github.com/graphiti-api/graphiti/commit/35acc3e6c78f68eeeb05728ea16264b136455c30)), closes [#531](https://github.com/graphiti-api/graphiti/issues/531) +* let an abstract resource declare relationships ([6036392](https://github.com/graphiti-api/graphiti/commit/6036392ebd611c7ed492beb3fe5754433eac9ffd)), closes [#453](https://github.com/graphiti-api/graphiti/issues/453) +* reject a non-object data payload instead of raising TypeError ([51603b8](https://github.com/graphiti-api/graphiti/commit/51603b854270715b80a75d9aa05a4020e6bbdce5)) +* Update some missing sections and terms after renames, register unsupported pagination error ([39cff19](https://github.com/graphiti-api/graphiti/commit/39cff19f6f1f73126514c8b18234a18ad5eb7379)) + + +### Features + +* carry CurrentAttributes into concurrent sideloads ([7092096](https://github.com/graphiti-api/graphiti/commit/7092096f6d99ad277cc56817c616d946f655d55b)) +* fold allow_nil and deny_empty into one blanks: filter option ([a108403](https://github.com/graphiti-api/graphiti/commit/a108403e5cd87fd2fdf177da30c4c4f97b9fe750)) +* move link rendering onto the resource as page_links and relationship_links ([57878d1](https://github.com/graphiti-api/graphiti/commit/57878d14fdfc4741e8d418c8edbc4a8687335843)) +* move typecast_reads onto the resource and let the railtie own Rails config ([236e970](https://github.com/graphiti-api/graphiti/commit/236e9706d7d8f08e9640c5c22ac596e2f7466849)) +* one link mode per relationship, cascading from relationship_links ([f0e1da1](https://github.com/graphiti-api/graphiti/commit/f0e1da1e13a8d12b47458495b378779f5687ffe2)) +* point connection pool timeouts at the sizing formula and add an audit task to check ([6e017c6](https://github.com/graphiti-api/graphiti/commit/6e017c60fd45338e4e6c8527adebfd46be4bfd8d)) +* split validate_endpoints into validate_requests and validate_links ([100a370](https://github.com/graphiti-api/graphiti/commit/100a370deec6a0e6ae165bdf77e50b6b14d068d3)) + +# [2.0.0-beta.9](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.8...v2.0.0-beta.9) (2026-08-22) + + +### Bug Fixes + +* bridge the last 1.x names that died with a bare NameError ([34e1dbd](https://github.com/graphiti-api/graphiti/commit/34e1dbd3e34bb8e6be56292c51fcb32674f75995)) +* build the entity map when the root query is, not on first use ([134d18b](https://github.com/graphiti-api/graphiti/commit/134d18b12b9ebb6903cca813209f60e5626b25a6)) +* coerce debug flags from env strings ([46be6aa](https://github.com/graphiti-api/graphiti/commit/46be6aa05d4e2a9e5d6b2562f223b4b05582bb39)) +* keep a customized sideload's instances out of entity dedup ([b1beee3](https://github.com/graphiti-api/graphiti/commit/b1beee3288fae2b20b009983aaa2f6c71189e2ca)) +* raise a more helpful named error when belongs_to linkage reads an unselected foreign key ([f59ea0b](https://github.com/graphiti-api/graphiti/commit/f59ea0bda94b388b1c2c0ae0e50b258f8ce0eb11)) +* resolve inline when already on a sideload pool thread ([11c9de7](https://github.com/graphiti-api/graphiti/commit/11c9de7fb4bfc2d83b02aaba3e58c1f94c84cec4)) +* scope debugger chunks to the request rather than the class ([9a77896](https://github.com/graphiti-api/graphiti/commit/9a77896c6e56d23b89d0a2aef358d8e8e0c28c54)) +* surface the first child error from concurrent polymorphic sideloads ([5a08c0e](https://github.com/graphiti-api/graphiti/commit/5a08c0edab2bc1ee0c87c6abbe1067297569411e)) + + +### Performance Improvements + +* nest the entity map instead of keying it by an array ([20bf625](https://github.com/graphiti-api/graphiti/commit/20bf6259d0347b02568b7d47054bb799e69629c3)) +* skip the promise machinery when nothing is sideloaded ([d879923](https://github.com/graphiti-api/graphiti/commit/d879923070a35d1a0e908b2d04746aa81effb262)) +* skip zipping a lone sideload promise ([7ef2cea](https://github.com/graphiti-api/graphiti/commit/7ef2cea654c48175504105f907b5b651c9fcdaf3)) + +# [2.0.0-beta.8](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.7...v2.0.0-beta.8) (2026-08-10) + + +### Bug Fixes + +* accept a single value for array filters ([#517](https://github.com/graphiti-api/graphiti/issues/517)) ([ee685b6](https://github.com/graphiti-api/graphiti/commit/ee685b625e967a8225d48b486bd093e3a6970176)) +* accept a single value for array filters ([#517](https://github.com/graphiti-api/graphiti/issues/517)) ([23f3e80](https://github.com/graphiti-api/graphiti/commit/23f3e80b583b0bf0843d8bb9334970ae56d83299)) +* drop the relationship guard install notice ([65b8be6](https://github.com/graphiti-api/graphiti/commit/65b8be65455a7d034c9dca082434c1dbfe610a7a)) +* reject invalid page parameters ([#537](https://github.com/graphiti-api/graphiti/issues/537)) ([fbb5683](https://github.com/graphiti-api/graphiti/commit/fbb5683393301e17ffcea84990ac5a95bdd3e15f)), closes [#347](https://github.com/graphiti-api/graphiti/issues/347) +* treat empty polymorphic configuration as unset ([#538](https://github.com/graphiti-api/graphiti/issues/538)) ([a27eeea](https://github.com/graphiti-api/graphiti/commit/a27eeea6eeb72749376c1f7959cadc89012142b9)), closes [#199](https://github.com/graphiti-api/graphiti/issues/199) +* typecast null values in filter lists ([#515](https://github.com/graphiti-api/graphiti/issues/515)) ([3756fd1](https://github.com/graphiti-api/graphiti/commit/3756fd12c48d2fb3cf5e2d40c38aab92f12218b0)) +* typecast null values in filter lists ([#515](https://github.com/graphiti-api/graphiti/issues/515)) ([3dcf4c8](https://github.com/graphiti-api/graphiti/commit/3dcf4c868abb12eb6b6d59cd0814b227d4838ece)) + + +### Features + +* add full set of commented-out Resource defaults in ApplicationResource when using the generator ([367678e](https://github.com/graphiti-api/graphiti/commit/367678eb3c2c737331f8edb00e00e70483a778b4)) +* **generators:** explicit rendering by default, respond_with only when Responders is included ([70741fe](https://github.com/graphiti-api/graphiti/commit/70741fef2a3a585bbef32c3e535027ff610fda77)) +* in-tree plain ruby and sinatra examples ([b92c6d9](https://github.com/graphiti-api/graphiti/commit/b92c6d9af22f759cc1f1eaccc250ecd04901e2aa)) +* ship the Rails application template in this repo ([76d5329](https://github.com/graphiti-api/graphiti/commit/76d53291f94c576792e3649044784f88fbaa47a7)) + +# [2.0.0-beta.7](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.6...v2.0.0-beta.7) (2026-08-10) + + +### Features + +* dedupe under concurrency and across distinct resources ([800b1a2](https://github.com/graphiti-api/graphiti/commit/800b1a2bf9bb59702efd00afe8e4f54ae06b500e)) +* deduplicate sideloaded entities across include paths ([0c2829a](https://github.com/graphiti-api/graphiti/commit/0c2829a634e9b96e2403ff168be9a48c6a663c78)) +* deprecate SpecHelpers::Sugar in favor of the full helper names ([b155849](https://github.com/graphiti-api/graphiti/commit/b155849f55fe84b2d0911fa4681d82682dfa39ae)) + +# [2.0.0-beta.6](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.5...v2.0.0-beta.6) (2026-08-09) + + +### Bug Fixes + +* reject invalid page parameters ([#537](https://github.com/graphiti-api/graphiti/issues/537)) ([bb3698b](https://github.com/graphiti-api/graphiti/commit/bb3698b52911c658c5d023ad8fd55b5106195001)), closes [#347](https://github.com/graphiti-api/graphiti/issues/347) +* treat empty polymorphic configuration as unset ([#538](https://github.com/graphiti-api/graphiti/issues/538)) ([5c17899](https://github.com/graphiti-api/graphiti/commit/5c1789993fbb4638d4443cf014b8bef358addb80)), closes [#199](https://github.com/graphiti-api/graphiti/issues/199) + + +### Features + +* Add graphiti:audit task to audit resources for issues ([84436ea](https://github.com/graphiti-api/graphiti/commit/84436eab7c7d0713ec6f355b62ba8b06913ee31a)) +* raise MissingRelationshipMethod when rendering reads an association the model does not define ([1c41cee](https://github.com/graphiti-api/graphiti/commit/1c41cee787b4f747d2906b2230f8fb926e3d887d)) +* rename always_include_resource_ids to resource_ids, with a belongs_to_resource_ids_by_default setting ([8b11151](https://github.com/graphiti-api/graphiti/commit/8b111516822f6fc6e3675b59941688791cb34e74)) + +# [2.0.0-beta.5](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.4...v2.0.0-beta.5) (2026-08-07) + + +### Bug Fixes + +* drop the relationship guard install notice ([c204312](https://github.com/graphiti-api/graphiti/commit/c20431214e126cb8e72c82022ee4c852b10a024b)) +* keep rake task helpers out of the global namespace ([0a41a60](https://github.com/graphiti-api/graphiti/commit/0a41a6022fa4c9151948714c31db367aee330e22)), closes [graphiti-api/graphiti-rails#91](https://github.com/graphiti-api/graphiti-rails/issues/91) + + +### Features + +* add rspec matchers for resource relationships and attributes ([70525f7](https://github.com/graphiti-api/graphiti/commit/70525f7e10c511aa0c867ecc873f63f1790b8247)), closes [graphiti-api/graphiti_spec_helpers#14](https://github.com/graphiti-api/graphiti_spec_helpers/issues/14) +* let the resource generator name the controller ([6ea7141](https://github.com/graphiti-api/graphiti/commit/6ea714181f3da011cae9f42477b4c3681a7a90ff)), closes [graphiti-api/graphiti-rails#53](https://github.com/graphiti-api/graphiti-rails/issues/53) + +# [2.0.0-beta.4](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.3...v2.0.0-beta.4) (2026-08-07) + + +### Bug Fixes + +* a subclass redeclaring a relationship reaches its serializer ([976dbc4](https://github.com/graphiti-api/graphiti/commit/976dbc47e7bbf951da925a13a34f1bc9679470f8)) +* make each appraisal test the Rails version it is named for ([9e00652](https://github.com/graphiti-api/graphiti/commit/9e00652e124a411a2916d0fef52dc4fd2dc59020)) +* require active_support so graphiti boots without Rails ([d431a03](https://github.com/graphiti-api/graphiti/commit/d431a0328fb4071429f39f4c69cda9c6cee308f2)) + + +### Code Refactoring + +* bridge the remaining 1.x names ([3d80ef9](https://github.com/graphiti-api/graphiti/commit/3d80ef98cf765a152032b7fed89b3cdc10be23dc)) + + +### Features + +* belongs_to renders resource linkage by default ([024824d](https://github.com/graphiti-api/graphiti/commit/024824d319619811ade886c72ce345cfeb584dbc)), closes [#168](https://github.com/graphiti-api/graphiti/issues/168) [#185](https://github.com/graphiti-api/graphiti/issues/185) [#167](https://github.com/graphiti-api/graphiti/issues/167) [#167](https://github.com/graphiti-api/graphiti/issues/167) +* fold graphiti_spec_helpers into graphiti ([da955a1](https://github.com/graphiti-api/graphiti/commit/da955a1e090ba44be31bf043ed90baa6661a4772)) +* fold graphiti-rails into graphiti ([7740f8a](https://github.com/graphiti-api/graphiti/commit/7740f8a5803f1fbf02ef4e27d8baa6e0809d2b43)), closes [graphiti-rails#52](https://github.com/graphiti-rails/issues/52) +* handle exceptions with rescue_registry, fold in graphiti_errors ([e48171c](https://github.com/graphiti-api/graphiti/commit/e48171c117e7b8141fb6f966a972b0c1cd50c5ab)) +* require Ruby 3.2 and Rails 7.1 ([635b249](https://github.com/graphiti-api/graphiti/commit/635b249a99f7d1ef726ae2328707ee6210cc0608)) + + +### BREAKING CHANGES + +* nothing removed, everything warns and goes away in 3.0. Except `include GraphitiErrors`, which now raises, as rescue_registry replaced it, so there's nothing to point it at. +* graphiti_errors is no longer a dependency and must be removed from the Gemfile, along with any `include GraphitiErrors`. + +GraphitiErrors::Validation::Serializer is now Graphiti::ErrorSerializers::Validation, and GraphitiErrors.enable!/disable! becomes handle_request_exceptions. 409 responses now report code "conflict" and title "Conflict Error". +* Ruby >= 3.2 and Rails >= 7.1 are now required. +* remove graphiti-rails from your Gemfile. Controllers serving Graphiti resources must `include Graphiti::Rails::Controller` — previously every controller received it whether it wanted it or not. Graphiti::Responders is now Graphiti::Rails::Responders. +* remove graphiti_spec_helpers from your Gemfile. Prefer Graphiti::SpecHelpers and "graphiti/spec_helpers/rspec"; the old namespace and require paths still resolve, warn, and are removed in 3.0. + +# [2.0.0-beta.3](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.2...v2.0.0-beta.3) (2026-07-31) + + +### Features + +* carry the assigned model on the resource, not through override signatures ([8ad848d](https://github.com/graphiti-api/graphiti/commit/8ad848d9bcd24cd63efb67e8ce32d5c8f3cfe147)) + +# [2.0.0-beta.2](https://github.com/graphiti-api/graphiti/compare/v2.0.0-beta.1...v2.0.0-beta.2) (2026-07-30) + + +### Features + +* add Resource.wrap as an easy way to use graphiti serialization on models fetched via other means (and not via graphiti's finders) ([#513](https://github.com/graphiti-api/graphiti/issues/513)) ([fcd19e2](https://github.com/graphiti-api/graphiti/commit/fcd19e2d023091d85947dbdd89259b587e0f582b)) +* deprecate mutating attributes in around_persistence hooks ([#514](https://github.com/graphiti-api/graphiti/issues/514)) [skip ci] ([8410ab0](https://github.com/graphiti-api/graphiti/commit/8410ab0a916077601cf85d8260a05ef9098149eb)) + +# [2.0.0-beta.1](https://github.com/graphiti-api/graphiti/compare/v1.12.2...v2.0.0-beta.1) (2026-07-30) + + +### Features + +* drop Ruby 2.7 and Rails 5.2 support ([e905ddb](https://github.com/graphiti-api/graphiti/commit/e905ddb5c842299ffbfe5ec3c4ff98ec9b603a51)) +* the model you inspect is the model that saves ([#465](https://github.com/graphiti-api/graphiti/issues/465)) ([a905fff](https://github.com/graphiti-api/graphiti/commit/a905fffc3a2aa5e663ff4cadcd632e68f053c81a)) + + +### BREAKING CHANGES + +* around_persistence hooks receive the assigned model instead of the attributes hash. Move attribute-hash modifications to before_attributes, or set values on the model. Custom create/update overrides that should receive a pre-assigned model must accept an assigned_model: keyword. See UPGRADING.md +* Ruby >= 3.0 / Rails >= 6 are now required. + ## [1.13.4](https://github.com/graphiti-api/graphiti/compare/v1.13.3...v1.13.4) (2026-08-20) @@ -31,6 +259,7 @@ graphiti changelog * accept a single value for array filters ([#517](https://github.com/graphiti-api/graphiti/issues/517)) ([23f3e80](https://github.com/graphiti-api/graphiti/commit/23f3e80b583b0bf0843d8bb9334970ae56d83299)) * typecast null values in filter lists ([#515](https://github.com/graphiti-api/graphiti/issues/515)) ([3dcf4c8](https://github.com/graphiti-api/graphiti/commit/3dcf4c868abb12eb6b6d59cd0814b227d4838ece)) + # [1.13.0](https://github.com/graphiti-api/graphiti/compare/v1.12.2...v1.13.0) (2026-07-30) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d98287f1..4e0a3156 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,7 +65,7 @@ The Rails integration specs are gated behind an environment variable and are covering none of the ActiveRecord adapter: ```sh -BUNDLE_GEMFILE=gemfiles/rails_8_0_graphiti_rails.gemfile \ +BUNDLE_GEMFILE=gemfiles/rails_8_0.gemfile \ APPRAISAL_INITIALIZED=1 \ bundle exec rspec spec/integration/rails/ ``` @@ -75,3 +75,5 @@ Lint with: ```sh bundle exec standardrb ``` + +CI fails PRs that don't pass lint, and almost everything standard flags is auto-fixable — run `bundle exec standardrb --fix` before pushing. (A bare `rake` also runs lint before the specs.) diff --git a/Gemfile b/Gemfile index 6d80a9aa..b2cbaec9 100644 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,8 @@ source "https://rubygems.org" # Specify your gem's dependencies in graphiti.gemspec gemspec +gem "rescue_registry", "~> 1.1" + group :test do gem "database_cleaner" gem "pry" diff --git a/README.md b/README.md index 1e2346a1..b3ae29c7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#### Graphiti +### Graphiti [![CI](https://github.com/graphiti-api/graphiti/actions/workflows/ci.yml/badge.svg)](https://github.com/graphiti-api/graphiti/actions/workflows/ci.yml) [![Gem Version](https://badge.fury.io/rb/graphiti.svg)](https://badge.fury.io/rb/graphiti) @@ -7,222 +7,40 @@ [![discord](https://img.shields.io/badge/community-discord-8A2BE2?logo=discord)](https://discord.gg/wgqkMBsSRV) -[![guides](https://img.shields.io/badge/guides-https://www.graphiti.dev-F565A5)](https://www.graphiti.dev) +[![guides](https://img.shields.io/badge/guides-https://graphiti.dev-F565A5)](https://graphiti.dev) Graphiti logo -Graphiti is a resource-oriented framework that sits on top of your models (usually ActiveRecord) and exposes them via a JSON:API-compliant interface. It abstracts common concerns like serialization, filtering, sorting, pagination, and sideloading relationships, so you can build powerful APIs with minimal boilerplate. By defining resources instead of controllers and serializers, Graphiti helps you keep your API logic organized, consistent, and easy to maintain. +Graphiti sits on top of your models (usually ActiveRecord) and exposes them over a JSON:API-compliant interface. You define Resources instead of controllers and serializers, and get filtering, sorting, pagination, sparse fieldsets, statistics, and nested reads and writes across relationships, all over one endpoint. -#### Examples -Here's an example resource from the [example app](https://github.com/graphiti-api/employee_directory/) just to give you a taste of the possibilities. +It's built on the [JSON:API](https://jsonapi.org) spec, which settles the decisions every API accumulates: response shapes, filtering, sorting, pagination, error formats, and how related data rides along. Your client layer speaks this protocol in return. It isn't complicated, so client logic can be hand-rolled or you can use one of the [many available libraries](https://jsonapi.org/implementations/#client-libraries) that work with the standard. +A Resource looks like this: ```ruby class EmployeeResource < ApplicationResource attribute :first_name, :string attribute :last_name, :string attribute :age, :integer - attribute :created_at, :datetime, writable: false - attribute :updated_at, :datetime, writable: false - attribute :title, :string, only: [:filterable, :sortable] has_many :positions - has_many :tasks - many_to_many :teams - polymorphic_has_many :notes, as: :notable - has_one :current_position, resource: PositionResource do - params do |hash| - hash[:filter][:current] = true - end - end - - filter :title, only: [:eq] do - eq do |scope, value| - scope.joins(:current_position).merge(Position.where(title: value)) - end - end - - sort :title do |scope, value| - scope.joins(:current_position).merge(Position.order(title: value)) - end - - sort :department_name, :string do |scope, value| - scope.joins(current_position: :department) - .merge(Department.order(name: value)) - end end ``` -A pretty boilerplate controller that just interfaces with the resource -```ruby -class EmployeesController < ApplicationController - def index - employees = EmployeeResource.all(params) - respond_with(employees) - end - - def show - employee = EmployeeResource.find(params) - respond_with(employee) - end - - def create - employee = EmployeeResource.build(params) - - if employee.save - render jsonapi: employee, status: 201 - else - render jsonapi_errors: employee - end - end - - def update - employee = EmployeeResource.find(params) - - if employee.update_attributes - render jsonapi: employee - else - render jsonapi_errors: employee - end - end - - def destroy - employee = EmployeeResource.find(params) - - if employee.destroy - render jsonapi: { meta: {} }, status: 200 - else - render jsonapi_errors: employee - end - end -end -``` - - - - -Now you can query your endpoints simply and powerfully, like: - - - -Request: -```http://localhost:3000/api/v1/employees?filter[title][eq]=Future Government Administrator&filter[age][lt]=40``` - -
-JSON-API response - -```json -{ - "data": [ - { - "id": "1", - "type": "employees", - "attributes": { - "first_name": "Quinn", - "last_name": "Homenick", - "age": 36, - "created_at": "2025-03-21T23:04:40+00:00", - "updated_at": "2025-03-21T23:04:40+00:00" - }, - "relationships": { - "positions": { - "links": { - "related": "/api/v1/positions?filter[employee_id]=1" - }, - "data": [ - { - "type": "positions", - "id": "1" - }, - { - "type": "positions", - "id": "2" - } - ] - }, - "tasks": { - "links": { - "related": "/api/v1/tasks?filter[employee_id]=1" - } - }, - "teams": { - "links": { - "related": "/api/v1/teams?filter[employee_id]=1" - } - }, - "notes": { - "links": { - "related": "/api/v1/notes?filter[notable_id]=1&filter[notable_type][eql]=Employee" - } - }, - "current_position": { - "links": { - "related": "/api/v1/positions?filter[current]=true&filter[employee_id]=1" - }, - "data": { - "type": "positions", - "id": "1" - } - } - } - } - ], - "included": [ - { - "id": "1", - "type": "positions", - "attributes": { - "title": "Future Government Administrator", - "active": true - }, - "relationships": { - "employee": { - "links": { - "related": "/api/v1/employees/1" - } - }, - "department": { - "links": { - "related": "/api/v1/departments/3" - } - } - } - }, - { - "id": "2", - "type": "positions", - "attributes": { - "title": "Manufacturing Specialist", - "active": false - }, - "relationships": { - "employee": { - "links": { - "related": "/api/v1/employees/1" - } - }, - "department": { - "links": { - "related": "/api/v1/departments/2" - } - } - } - } - ], - "meta": {} -} -``` +That Resource serves `?filter[age][gt]=30`, `?sort=-age`, `?page[size]=10`, `?include=positions` and more, without writing any of them. See [graphiti.dev](https://graphiti.dev/) for the whole loop, or the [example app](https://github.com/graphiti-api/employee_directory/) for a full working API. -
+### Documentation +Docs live at [graphiti.dev](https://graphiti.dev/) and are rendered from this repo: the markdown is in [`/docs`](docs), and the Docusaurus site that serves it is in [`/website`](website). Fixes and improvements are welcome (every page has an "Edit this page" link at the bottom, which makes opening a PR with a change easy). +To preview locally: -[Graphiti Guides](https://www.graphiti.dev/guides/) +```bash +cd website +npm install +npm run start +``` [Join the Discord](https://discord.gg/wgqkMBsSRV) - - - - diff --git a/Rakefile b/Rakefile index ce064ce2..2c61ab40 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,9 @@ require "bundler/gem_tasks" require "rspec/core/rake_task" require "appraisal" +# Standard is silent on success; show the inspected-files summary. +ENV["STANDARDOPTS"] ||= "--format progress" +require "standard/rake" RSpec::Core::RakeTask.new(:spec) do |t| if ENV["APPRAISAL_INITIALIZED"] @@ -8,8 +11,41 @@ RSpec::Core::RakeTask.new(:spec) do |t| end end -if !ENV["APPRAISAL_INITIALIZED"] && !ENV["TRAVIS"] - task default: [:spec, :appraisal] -else +# Allocations mean the same anywhere and timings do not, so the history has a +# reference CPU, named on every row, and recording timings refuses to mix them. +namespace :performance do + # Shelling out keeps a failure to the script's own message, with no rake backtrace on top. + def measure(*arguments) + exit(1) unless system("bundle", "exec", "ruby", "spec/performance/measure_releases.rb", *arguments) + end + + desc "Read a change: measure the working tree, print the comparison and plot it" + task :read do + measure("--read") + end + + desc "Open the recorded history as a chart. Any machine, measures nothing" + task :chart do + exit(1) unless system("bundle", "exec", "ruby", "spec/performance/chart_page.rb", "--no-current") + end + + desc "Fill in the history. Missing releases by default, TAG=v2.0.0-beta.9 for one, ALL=1 for every. Reference CPU" + task :record do + measure(*(if ENV["TAG"] + [ENV["TAG"]] + else + ENV["ALL"] ? ["--all"] : ["--missing"] + end)) + end + + desc "Measure the working tree for the release to name, and commit it. Reference CPU" + task :pending do + measure("--pending") + end +end + +if ENV["APPRAISAL_INITIALIZED"] task default: [:spec] +else + task default: [:standard, :spec, :appraisal] end diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 00000000..edbcc30f --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,5 @@ +# Upgrading Graphiti + +The upgrade guide lives with the rest of the documentation, so it can link into the concept pages and offer the same version picker: **https://graphiti.dev/upgrading** + +In this repository it is [docs/upgrading.md](docs/upgrading.md). diff --git a/deprecated_generators/graphiti/generator_mixin.rb b/deprecated_generators/graphiti/generator_mixin.rb deleted file mode 100644 index 237a9908..00000000 --- a/deprecated_generators/graphiti/generator_mixin.rb +++ /dev/null @@ -1,46 +0,0 @@ -module Graphiti - module GeneratorMixin - def prompt(header: nil, description: nil, default: nil) - say(set_color("\n#{header}", :magenta, :bold)) if header - say("\n#{description}") if description - answer = ask(set_color("\n(default: #{default}):", :magenta, :bold)) - answer = default if answer.blank? && default != "nil" - say(set_color("\nGot it!\n", :white, :bold)) - answer - end - - def api_namespace - @api_namespace ||= begin - ns = graphiti_config["namespace"] - ns.delete_suffix("/") - - if ns.blank? - ns = prompt \ - header: "What is your API namespace?", - description: "This will be used as a route prefix, e.g. if you want the route '/books_api/v1/authors' your namespace would be '/books_api/v1'", - default: "/api/v1" - update_config!("namespace" => ns) - end - - ns - end - end - - def actions - @options["actions"] || %w[index show create update destroy] - end - - def actions?(*methods) - methods.any? { |m| actions.include?(m) } - end - - def graphiti_config - File.exist?(".graphiticfg.yml") ? YAML.load_file(".graphiticfg.yml") : {} - end - - def update_config!(attrs) - config = graphiti_config.merge(attrs) - File.open(".graphiticfg.yml", "w") { |f| f.write(config.to_yaml) } - end - end -end diff --git a/deprecated_generators/graphiti/install_generator.rb b/deprecated_generators/graphiti/install_generator.rb deleted file mode 100644 index 7496ee4a..00000000 --- a/deprecated_generators/graphiti/install_generator.rb +++ /dev/null @@ -1,86 +0,0 @@ -require_relative "generator_mixin" - -module Graphiti - class InstallGenerator < ::Rails::Generators::Base - include GeneratorMixin - - source_root File.expand_path("../templates", __FILE__) - - class_option :'omit-comments', - type: :boolean, - default: false, - aliases: ["-c"], - desc: "Generate without documentation comments" - - desc "This generator boostraps graphiti" - def install - to = File.join("app/resources", "application_resource.rb") - template("application_resource.rb.erb", to) - - inject_into_file "app/controllers/application_controller.rb", after: "class ApplicationController < ActionController::API\n" do - app_controller_code - end - - inject_into_file "app/controllers/application_controller.rb", after: "class ApplicationController < ActionController::Base\n" do - app_controller_code - end - - inject_into_file "config/application.rb", after: "Rails::Application\n" do - <<-'RUBY' - # In order for Graphiti to generate links, you need to set the routes host. - # When not explicitly set, via the HOST env var, this will fall back to - # the rails server settings. - # Rails::Server is not defined in console or rake tasks, so this will only - # use those defaults when they are available. - routes.default_url_options[:host] = ENV.fetch('HOST') do - if defined?(Rails::Server) - argv_options = Rails::Server::Options.new.parse!(ARGV) - "http://#{argv_options[:Host]}:#{argv_options[:Port]}" - end - end - RUBY - end - - inject_into_file "spec/rails_helper.rb", after: /RSpec.configure.+^end$/m do - "\n\nGraphitiSpecHelpers::RSpec.schema!" - end - - insert_into_file "config/routes.rb", after: "Rails.application.routes.draw do\n" do - if defined?(VandalUi) - <<-STR - scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do - mount VandalUi::Engine, at: '/vandal' - # your routes go here - end - STR - else - <<-STR - scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do - # your routes go here - end - STR - end - end - end - - private - - def omit_comments? - @options["omit-comments"] - end - - def app_controller_code - str = "" - str << " include Graphiti::Rails\n" - str << " include Graphiti::Responders\n" - str << "\n" - str << " register_exception Graphiti::Errors::RecordNotFound,\n" - str << " status: 404\n" - str << "\n" - str << " rescue_from Exception do |e|\n" - str << " handle_exception(e)\n" - str << " end\n" - str - end - end -end diff --git a/deprecated_generators/graphiti/templates/application_resource.rb.erb b/deprecated_generators/graphiti/templates/application_resource.rb.erb deleted file mode 100644 index 1c5b8c3d..00000000 --- a/deprecated_generators/graphiti/templates/application_resource.rb.erb +++ /dev/null @@ -1,15 +0,0 @@ -<%- unless omit_comments? -%> -# ApplicationResource is similar to ApplicationRecord - a base class that -# holds configuration/methods for subclasses. -# All Resources should inherit from ApplicationResource. -<%- end -%> -class ApplicationResource < Graphiti::Resource - <%- unless omit_comments? -%> - # Use the ActiveRecord Adapter for all subclasses. - # Subclasses can still override this default. - <%- end -%> - self.abstract_class = true - self.adapter = Graphiti::Adapters::ActiveRecord - self.base_url = Rails.application.routes.default_url_options[:host] - self.endpoint_namespace = '<%= api_namespace %>' -end diff --git a/docs/concepts/backends-and-models.md b/docs/concepts/backends-and-models.md new file mode 100644 index 00000000..7d3313eb --- /dev/null +++ b/docs/concepts/backends-and-models.md @@ -0,0 +1,122 @@ +--- +title: 'Backends and Models' +--- + +# Backends and Models + +A Resource queries a **Backend** and returns **Models** from what comes back. Graphiti serializes the Models. + +With ActiveRecord those are the same object. `Employee` is both the thing you query and the thing you render, and you can skip most of this page. It matters when they're separate: a search index, an HTTP service, a document store. Then the Backend is whatever you query, and the Model is whatever you hand back. + +## Scopes {#scopes} + +A **scope** is whatever your backend needs to run a query. Graphiti doesn't care what it is. For ActiveRecord it's an `ActiveRecord::Relation`. Here it's a plain hash: + +```ruby +class EmployeeResource < ApplicationResource + self.adapter = Graphiti::Adapters::Null + + attribute :name, :string + + def base_scope + { conditions: {}, sort: {} } + end + + filter :name do + eq do |scope, value| + scope[:conditions].merge!(value) + scope + end + end + + sort :name do |scope, direction| + scope[:sort] = { name: direction } + scope + end + + def resolve(scope) + results = Backend.query(scope) + results.map { |result| Employee.new(result) } + end +end +``` + +`base_scope` is the starting point, each `filter` and `sort` block mutates it based on request params, and `resolve` runs the query and returns Models. + +**Every block must return the scope.** Returning the result of `merge!` or an assignment instead of the scope itself is the most common way to break this. + +Writing that per Resource gets old. Once the pattern stabilizes, move it into an [Adapter](/topics/without-activerecord#adapters) and Resources go back to being declarative: + +```ruby +class EmployeeResource < ApplicationResource + self.adapter = BackendAdapter + attribute :name, :string +end +``` + +## What a Model has to do {#model-requirements} + +**Respond to `id`, uniquely.** Graphiti uses `model.id` to tell records apart when rendering. Duplicate ids produce wrong output, not an error. + +If the underlying record has no id, generate one: + +```ruby +def id + @id ||= SecureRandom.uuid +end +``` + +**Respond to its readable attributes.** `attribute :name, :string` calls `model.name`. If your Model doesn't have that method, pass a block instead: + +```ruby +attribute :name, :string do + @object.full_name +end +``` + +**Include `ActiveModel::Validations` if you want validation errors.** Graphiti checks models on write requests and renders a [JSON:API errors payload](http://jsonapi.org/format/#errors) from `model.errors`. Without it, an invalid model saves silently: + +```ruby +class Employee + include ActiveModel::Validations + + validates :name, presence: true +end +``` + +## Writing a Model {#model-implementations} + +Graphiti has no opinion here. A plain class works: + +```ruby +class Employee + attr_accessor :id, :first_name, :last_name, :age + + def initialize(attrs = {}) + attrs.each_pair { |key, value| send(:"#{key}=", value) } + end +end +``` + +[ActiveModel::Model](https://api.rubyonrails.org/classes/ActiveModel/Model.html) gives you the constructor and validations for free: + +```ruby +class Employee + include ActiveModel::Model + + attr_accessor :id, :first_name, :last_name, :age +end +``` + +[Dry::Struct](https://dry-rb.org/gems/dry-struct) adds type enforcement, and dry-types is already a Graphiti dependency: + +```ruby +class Employee < Dry::Struct + attribute :id, Types::Integer + attribute :first_name, Types::String + attribute :last_name, Types::String + attribute :age, Types::Integer +end +``` + +`OpenStruct` also works and is what Graphiti uses internally for remote resources, but it fails quietly in ways the others don't. See [OpenStruct Models](/topics/openstruct-models) before reaching for it. diff --git a/docs/concepts/endpoints.md b/docs/concepts/endpoints.md new file mode 100644 index 00000000..1f845c1d --- /dev/null +++ b/docs/concepts/endpoints.md @@ -0,0 +1,192 @@ +--- +title: 'Endpoints' +--- + +## Overview {#overview} + +**Endpoints** expose and customize +[Resources](/concepts/resources). + +Resources themselves can operate completely independently of a request or response: + +```ruby +employees = EmployeeResource.all({ + filter: { title: 'engineer' }, + sort: '-created_at', + page: { size: 10 }, + include: 'positions.department' +}) + +employees.map(&:first_name) # => ['Jane', 'John', ...] +employees.to_json # => { employees: [{ ... }] } +``` + +And Resources connect to other Resources. Our graph of data is defined +**outside** of the actual API. + +Endpoints expose this graph to the world. We might choose to have a `/employees` endpoint that can eager load comments (`?include=comments`), but never expose `/comments` directly. Or, we could do the opposite: expose lazy-loading `/comments`, but disallow eager loading from `/employees`. We can add caching rules, or add an `/exemplary_employees` endpoint with special query overrides. + +Finally, Endpoints are in charge of the [HTTP specification](https://tools.ietf.org/html/rfc2616): +request processing, response codes, caching, MIME types, and so on. If you're thinking +Rails, an Endpoint is the combination of a Route and Controller. + +### Endpoint Logic {#endpoint-logic} + +Often, you won't need to customize Endpoints - especially if you're +using our [Rails Resource +generator](/concepts/resources#generators). Endpoint logic mostly +concerns: + +* Caching +* Side-effect behavior specific to the endpoint (e.g.: sending a +welcome email from `/users#create` but not `/admin/users#create`) +* Authorization (e.g `before_action`) +* Custom query parameter handling +* Validation handling +* Error handling +* Limiting Resource behavior +* Customizing Resource behavior + +If your logic falls elsewhere, consider a Resource or Model. + +### Rails Integration {#rails-integration} + +When using Rails, an endpoint is the combination of a Route and +Controller: + +```ruby +# config/routes.rb +resources :posts, only: [:index] + +# app/controllers/posts_controller.rb +class PostsController < ApplicationController + def index + posts = PostResource.all(params) + + respond_to do |format| + format.jsonapi { render jsonapi: posts } + format.json { render json: posts } + end + end +end +``` + +You'll note that Graphiti hooks into Rails with a mixin (set when using +our application generator): + +```ruby +class ApplicationController < ActionController::API + include Graphiti::Rails::Controller + + # ... code ... +end +``` + +This gives us [#sideload_allowlist](#sideload-allowlist), sets the +[context](/concepts/resources#context), and makes `respond_to` available +in API-only controllers. + +## Customizing Resources {#customizing-resources} + +### Scope Overrides {#scope-overrides} + +One common use case for endpoints is customizing the Resource +[base scope](/concepts/resources#base-scope). This causes a new +"starting point" for query building. + +Consider the endpoints `/posts` (basic CRUD) and `/top_posts`. Though both are associated to PostResource, `/top_posts` ensures that only +Posts with a certain number of upvotes get returned: + +```ruby +def index + base_scope = Post.where("upvotes > ?", 100) + posts = PostResource.all(params, base_scope) + + respond_to do |format| + format.jsonapi { render jsonapi: posts } + format.json { render json: posts } + end +end +``` + +We're able to reuse all the other logic in PostResource - relationships, +filters, sorts, etc - while only returning "Top Posts". + +### Sideload Allowlist {#sideload-allowlist} + +Resources define relationships to other resources. But we may not want +all of those relationships exposed at a given endpoint. + +Let's say we've defined relationships: + +`Employee > Position > Department > Hardware > CostHistory` + +It's reasonable to get an Employee, their Positions, and Departments for +those positions in a single request. But is it really valid to *also* pull down +all the hardware, as well as all the historical data on the cost of that hardware, +in a single request? Allowing the entire graph to be pulled down in a single request can cause excessive load on our +servers (and this is probably a better fit for lazy-loading via +[Links](/concepts/links)). + +Let's instead say that if we're entering the graph at `/employees`, the +furthest we can go is Department: + +```ruby +class EmployeesController < ApplicationController + self.sideload_allowlist = { + index: { positions: 'department' } + } + + # ... code ... +end +``` + +## Caching {#caching} + +### Etags {#etags} + +[ETags](https://robots.thoughtbot.com/introduction-to-conditional-http-caching-with-rails) are an important concept that is often overlooked. Etags tell browsers +that the response to a GET request hasn't changed since the last request and +can be safely pulled from the browser cache. If you care about sparse fieldsets, +you should care about ETags - if you're limiting fields to reduce payload size, +how about a payload size of **zero**? + +ETags are set by default in Rails, by checking the response body. This won't prevent queries from executing, but it will save clients from downloading the response again if nothing has changed. + +Let's manually set an ETag: + +```ruby +def index + posts = PostResource.all(params) + + if stale?(posts.data) + render jsonapi: posts + end +end +``` + +From the [documentation on #stale?](https://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-stale-3F): + +> *In this case last_modified will be set by calling `maximum(:updated_at)` on the collection (the timestamp of the most recently updated record) and the etag by passing the object itself.* + +Also consider the use case where data is ingested hourly. We can avoid a +query altogether by checking when the last ingestion ran: + +```ruby +def index + if stale?(EmployeeIngestion.last) + employees = EmployeeResource.all(params) + render jsonapi: employees + end +end +``` + +> **CAVEAT**: When setting ETags, consider sideloads. In the above examples +> we are checking to see the last update of an Employee, but we may be +> sideloading (and filtering) Positions as well. Use custom endpoints or +> [Sideload Allowlist](#sideload-allowlist) to mitigate this issue. + +## Testing {#testing} + +If you have custom Endpoint logic, we suggest testing using an [API +Test](/topics/testing#api-tests). diff --git a/docs/concepts/links.md b/docs/concepts/links.md new file mode 100644 index 00000000..90c66f3b --- /dev/null +++ b/docs/concepts/links.md @@ -0,0 +1,242 @@ +--- +title: 'Links' +--- + +# Links + +## Overview {#overview} + +A [Link](http://jsonapi.org/format/#document-links) is a URL Graphiti puts in a relationship, pointing at the data so a client can fetch it separately. Every relationship gets one automatically: + +```ruby +class PostResource < ApplicationResource + has_many :comments +end +``` + +`GET /posts/123` renders the `comments` relationship with a `links.related` of `/comments?filter[post_id]=123`. The client follows that URL when it wants the comments, rather than asking for them up front with `?include=comments`. + +### Why links {#why-links} + +The URL matters most when the relationship means something more specific than "all the comments". Say `top_comments` is defined as 100 upvotes or more. A [`params` block](#linking-relationships) puts that into the generated Link, and the client still follows a URL. + +The alternative is for clients to build that query themselves, which means every client (desktop, mobile, third-party) has to know what a "Top Comment" is and ship an update whenever the definition changes. Hiding it behind a dedicated `/top_comments` endpoint moves the problem rather than solving it: clients still have to know to hit a special endpoint, and nothing keeps its definition in sync with the eager-loaded one. + +With a Link, the definition lives in one place. Change it to 500 upvotes, factor in recency, subtract downvotes: clients keep following the same URL. + + +## Linking Relationships {#linking-relationships} + +When defining a relationship, we get a Link for free: + +```ruby +class PostResource < ApplicationResource + has_many :comments +end +``` + +> `/comments?filter[post_id]=123` + +And when customizing a relationship with `params`, our Link will be +updated: + +```ruby +has_many :comments do + params do |hash| + hash[:filter][:upvotes] = { gte: 100 } + end +end +``` + +> `/comments?filter[post_id]=123&filter[upvotes][gte]=100` + +Note: if you use the `scope` block directly, it may cause incorrect links. Avoid using `scope` directly and instead use `params` and `pre_load` if possible. + +To manually generate a Link: + +```ruby +has_many :comments do + link do |post| + helpers = Rails.application.routes.url_helpers + helpers.comments_url(params: { filter: { post_id: post.id } }) + # or + # http://example.com/api/v1/comments?filter[post_id]=123 + end +end +``` + +Every relationship link has one of three modes: `true` (always rendered), `false` (no link at all), or `:on_demand` (rendered when the request asks with `?links=true`). The resource's [`relationship_links`](#relationship-links) sets the default mode, and the `link:` option overrides it per relationship: + +```ruby +has_many :comments, link: false # no link, whatever the resource default +has_many :comments, link: :on_demand # only with ?links=true +``` + +## Resource Endpoints {#resource-endpoints} + +To generate links, we need to associate a Resource to a URL. By default, +this happens automatically: + +```ruby +class ApplicationResource < Graphiti::Resource + # ... code ... + self.endpoint_namespace = '/api/v1' +end + +class PostResource < ApplicationResource + # under the hood: + primary_endpoint 'posts', + [:index, :show, :create, :update, :destroy] +end +``` + +Which would generate links to `/api/v1/posts`. + +### Validation {#validation} + +Associating a Resource to an Endpoint serves two purposes. We've gone +over link generation. But we also want to make sure we're not linking to +something that doesn't actually exist. That's why we perform **Endpoint +Validation**. + +If we tried to access the above resource at a `/comments` endpoint: + +```ruby +class CommentsController < ApplicationController + def index + PostResource.all(params) + # ... + end +end +``` + +We'd get a `Graphiti::Errors::InvalidEndpoint` error. Endpoint +validation ensures that our auto-generated Links are actually valid. + +To change the endpoint associated to a Resource: + +```ruby +primary_endpoint 'special_posts', [:index, :show] +``` + +Or to alter only the **path**: + +```ruby +self.endpoint[:path] = 'special_posts' +``` + +Or to alter only the **actions** supported: + +```ruby +self.endpoint[:actions] = [:index, :show] +``` + +A resource may be accessible by multiple endpoints. Maybe `PostResource` is also used at `/top_posts`. We want to keep all auto-generated links pointing to `/posts` (the primary endpoint), but *allow* accessing `PostResource` from the `/top_posts` endpoint: + +```ruby +secondary_endpoint '/top_posts', [:index] +``` + +## Configuration {#configuration} + +### Relationship Links {#relationship-links} + +`relationship_links` is the default mode for every relationship link on the resource, taking the same three values as the per-relationship `link:` option. To turn links off unless a relationship opts in: + +```ruby +class ApplicationResource < Graphiti::Resource + self.relationship_links = false +end + +class PostResource < ApplicationResource + has_many :comments # no link + has_many :top_comments, link: true # rendered +end +``` + +A relationship with a custom `link do ... end` block is treated as `link: true` under a `false` default, on the theory that writing the block means wanting the link. + +(`self.autolink` was the 1.x name for the `false`/`true` half of this setting. It still works, warns, and will be removed in 3.0.) + +### Endpoint Validation {#endpoint-validation} + +Endpoints are validated in two directions, each with its own setting. + +`validate_requests` guards what comes in. A Resource refuses to serve a request whose path and action are not among its [endpoints](#resource-endpoints), which is what stops one Resource being reached through another's route: + +```ruby +class ApplicationResource < Graphiti::Resource + self.validate_requests = false +end +``` + +`validate_links` guards what goes out. Before rendering a relationship link, Graphiti checks that the target endpoint is actually routable for the action the link needs, which is `:show` for a `belongs_to` and `:index` otherwise. You never serialize a link that 404s. Custom `link do ... end` blocks and remote Resources are skipped: + +```ruby +class ApplicationResource < Graphiti::Resource + self.validate_links = false +end +``` + +Turn off `validate_links` when your links point at endpoints another service serves, and you still want the inbound guard. + +(`self.validate_endpoints` set both at once. It still works, warns, and will be removed in 3.0.) + +### Links-on-Demand {#links-on-demand} + +To only render relationship links when requested in the URL with `?links=true`: + +```ruby +class ApplicationResource < Graphiti::Resource + self.relationship_links = :on_demand +end +``` + +`relationship_links` accepts `true` (always render, the default), `false` (no links), or `:on_demand`. Set it on `ApplicationResource` to apply everywhere, on an individual resource to override, or per relationship with `link:`. A relationship with no link and no ids is left out of the payload. + +### Pagination Links {#pagination-links} + +The page params themselves, and cursors, are covered in [Pagination](/concepts/resources#pagination). + +Requesting large collections can make for slow responses. [Pagination](https://jsonapi.org/format/#fetching-pagination) breaks the response into smaller pieces, and pagination links tell the client how to walk them. They can appear in a response two ways. + +#### Showing by default {#pagination-links-showing-by-default} + +Every collection response returns pagination links: + +```ruby +class ApplicationResource < Graphiti::Resource + self.page_links = true +end +``` + +#### When requested {#pagination-links-when-requested} + +Links are rendered only when the request asks for them with `?page_links=true` (`?pagination_links=true` is still accepted). Worth doing when the collection is large: the `last` link needs a total count, so rendering links costs a `stat(:total, :count)` on every request that gets them. + +```ruby +class ApplicationResource < Graphiti::Resource + self.page_links = :on_demand +end +``` + +Like `relationship_links`, `page_links` accepts `true`, `false` (the default), or `:on_demand`, and can be set per resource. + +Pagination links won't show up for *#show* actions. + +### Custom Endpoint URLs {#custom-endpoint-urls} + +To change the URL associated with a Resource: + +```ruby +class PostResource < ApplicationResource + # Most commonly seen in ApplicationResource + self.endpoint_namespace = '/api/v1' + + primary_endpoint '/posts', [:index, :show] + # OR + self.endpoint[:path] = '/posts' + # OR + self.endpoint[:actions] = [:index, :show] +end +``` diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md new file mode 100644 index 00000000..6b0c5490 --- /dev/null +++ b/docs/concepts/overview.md @@ -0,0 +1,80 @@ +--- +title: 'Lifecycle of a Request' +--- + +# Lifecycle of a Request + +A request goes down through a Resource to your data, and comes back up as a serialized response. + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Request + Endpoint + Backend + JSON:API + + + Resource + + + + base_scope + filters, + sorts, pagination + resolve(scope) + to your Models + serialize + Adapter + + + response + + +
+ +Graphiti is the highlighted part: the Resource, and the JSON:API it renders. The Endpoint is your Rails controller, which handles routing, response codes and MIME types. The Backend is yours too. + +| Piece | What it does | +| --- | --- | +| [Endpoint](/concepts/endpoints) | Your controller. Graphiti registers its path and actions, which drives link generation and endpoint validation, and lets you vary a Resource's behavior per route. | +| [Resource](/concepts/resources) | Turns request params into a **scope**, resolves that scope into Models, and serializes them on the way back out. | +| Adapter | Reusable glue between a Resource and a Backend. Defaults to `Graphiti::Adapters::ActiveRecord`. | +| [Backend](/concepts/backends-and-models) | Whatever you query: a database, a search index, an HTTP service. | +| [Model](/concepts/backends-and-models) | What you return and serialize. With ActiveRecord, the same object as the Backend. | + +## The graph + +Resources connect to other Resources: + +* **Sideloading**: fetch an employee, her positions, and those positions' departments in one request +* **Sideposting**: *save* an employee and her positions in one request +* **[Links](/concepts/links)**: a URL to lazy-load positions in a separate request + +Query logic written for one Resource applies at every level of that graph, so you can ask for an employee and her last three positions ordered by `created_at`. That's [deep querying](/concepts/relationships#deep-queries). diff --git a/docs/concepts/persisting.md b/docs/concepts/persisting.md new file mode 100644 index 00000000..6350c4ef --- /dev/null +++ b/docs/concepts/persisting.md @@ -0,0 +1,376 @@ +--- +title: 'Persisting' +--- + +# Persisting {#persisting} + +This page covers how Graphiti writes data: the persistence lifecycle, sideposting a graph of resources in one request, validation errors, and reading data back after a write. + +Graphiti allows writing a graph of data in a single request. We'll do +the work of parsing the graph and ordering operations, so you can focus +on the part you care about: the logic for actually persisting an object. + +By default, persistence operations are handled by your adapter, and the flow breaks into three steps: build or find the model, assign attributes to it, then save it. + +Attributes are assigned up front, before the persistence hooks run. That means the model exists (populated but unwritten) before anything touches the database, and **the model you inspect is the model that saves**: + +```ruby +employee = EmployeeResource.build(payload) + +employee.data # the model, attributes already assigned, nothing written yet +employee.data.valid? # inspect it, or modify it +employee.save # persists that same instance +``` + +Reading `data` repeatedly returns the same instance, and the attribute callbacks run only once no matter how often you read it. For an update, the proxy reads the persisted record until you apply the payload: + +```ruby +proxy = EmployeeResource.find(payload) +proxy.data.first_name # => "asdf", straight from the database +proxy.assign_attributes(payload) +proxy.data.first_name # => "Jane", assigned but not yet persisted +proxy.save(action: :update) +``` + +`assign_attributes` validates the payload and runs your writable guards, but writes nothing. `#save` will not re-validate a payload it already validated, so inspecting the model costs no extra guard evaluations. `ResourceProxy#update` is the Rails-style shorthand that assigns and saves in one call. + +You can override `#create`, `#update` and `#destroy` on a Resource, but you are encouraged **not** to. Use the hooks below instead. If you do override them, `#create` and `#update` receive an attributes hash while `#destroy` receives an id, and all three **must return the Model instance**. Graphiti processes any `writable: false` or guarded attributes before these methods run, and checks the returned Model for validation errors afterward, rolling back the transaction if any Model in the graph is invalid. + +## Persistence Lifecycle Hooks {#persistence-lifecycle-hooks} + +Let's dive into a persistence request. If you look at the code snippets in +the prior section, the flow breaks down into 3 steps: + +* Build or find the model +* Assign attributes to the model +* Save + +You can hook into each step: + +```ruby +class PostResource < ApplicationResource + before_attributes do |attributes| + # Before attributes have been assigned to the model + end + + after_attributes do |model| + # After attributes have been assigned to the model + end + + around_attributes :do_around_attributes + + def do_around_attributes(attributes) + # before + model_instance = yield attributes + # after + end + + before_save do |model| + # After attributes assigned, but before persisting + end + + after_save do |model| + # After model has been saved + end + + around_save :do_around_save + + def do_around_save(model) + # before + yield model + # after + end + + # This is an *override* + # During #create, build a blank model instance + # By default, we'd call adapter.build(model_class) + def build(model_class) + model_class.new + end + + # This is an *override* + # During #create/#update, assign new attributes to the model instance + # By default, we'd call adapter.assign_attributes(model_instance, attributes) + def assign_attributes(model_instance, attributes) + attributes.each_pair do |key, value| + model_instance.send(:"#{key}=", value) + end + end + + # This is an *override* + # During #create/#update, actually save the model instance + # By default, we'd call adapter.save(model_instance) + def save(model_instance) + model_instance.save + model_instance + end + + + # This is an *override* + # During #destroy, actually save the model instance + # By default, we'd call adapter.destroy(model_instance) + def delete(model_instance) + model_instance.destroy + model_instance + end + + # Finally, you may want to hook around *all* the above steps: + # Only applies to #create/#update + around_persistence :do_around_persistence + + def do_around_persistence(attributes) + attributes[:foo] = 'bar' + model = yield # build/find, assign attrs, save + model.update_counter_cache + end +end +``` + +* All hooks have `only/except` options, e.g. `before_attributes only: [:update]` +* Most hooks can be called with an in-line block, or by passing a method +name (e.g. `before_attributes :do_something`). The exception is `around_*` hooks, which *must* be called with a method name. + +When persisting multiple objects at once, we'll open a database +transaction, process each model individually, ensure all models pass +validation, then close the transaction. This means that if you raise an +error at any point, or any model does not pass validations, the +transaction will be rolled back. + +You may want to perform an operation after all models have been +processed and validated, but before the transaction is closed. One +example is sending an email - you don't want to send if the models were +invalid, so `after_save` wouldn't work. And you still want to do it +*within* the transaction, so if your email server is down and an error +is raised the transaction gets rolled back. + +For this scenario, use `before_commit`: + +```ruby +before_commit do |model| + PostMailer.with(post: model).some_email.deliver +end +``` + +## Sideposting {#sideposting} + +The act of persisting multiple Resources in a single request is called +**Sideposting**. The payload mirrors the **sideloading** payload for +read operations, with minor additions. + +Let's create a Post and associate it to an existing Blog in a single +request: + +```ruby +# POST /api/v1/posts +{ + type: 'posts', + attributes: { title: 'My post' }, + relationships: { + blog: { + data: { + id: '1', + type: 'blogs', + method: 'update' + } + } + } +} +``` + +The critical addition here is the `method` key. When we persist RESTful +Resources, we send a corresponding HTTP verb. This follows the same +pattern, adding a verb for each Resource in the graph. `method` can be +one of: + + * `create` + * `update` + * `destroy` + * `disassociate` (e.g. `null` foreign key) + +When we sidepost, all objects will be persisted within the same database +transaction, which rolls back if an error is raised or any objects are invalid. + +### Create {#create} + +Let's say we want to create a Post and its Blog in a single request. +You'll note that we don't have the `id` key to generate a [Resource Identifier](http://jsonapi.org/format/#document-resource-identifier-objects) (combination of `id` and `type` +that uniquely identifies a Resource). + +To accomodate this, send an ephemeral `temp-id` (any UUID): + +```ruby +{ + # POST /api/v1/posts + { + type: 'posts', + attributes: { title: 'My post' }, + relationships: { + blog: { + data: { + :'temp-id' => 'abc123', + type: 'blogs', + method: 'create' + } + } + }, + included: [ + { + :'temp-id' => 'abc123' + type: 'blogs', + attributes: { name: 'New Blog' } + } + ] + } +} +``` + +This random UUID: + +* Connects relevant sections of the payload. +* Tells clients how to associate their in-memory objects with the ids returned from the server. + +### Expanded Example {#expanded-example} + +Here we're updating a Post, changing the name of its associated Blog, creating a Tag, deleting one Comment, and disassociating (`null` foreign key) a different Comment, all in a single request: + +```ruby +{ + data: { + type: 'posts', + id: 123, + attributes: { title: 'Updated!' }, + relationships: { + blog: { + data: { + type: 'blogs', + id: 123, + method: 'update' + } + }, + tags: { + data: [{ + type: 'tags', + temp-id: 's0m3uu1d', + method: 'create' + }] + }, + comments: { + data: [ + { + type: 'comments', + id: '123', + method: 'destroy' + }, + { + type: 'comments', + id: '456', + method: 'disassociate' + } + ] + } + } + }, + included: [ + { + type: 'tags', + :'temp-id' => 's0m3uu1d', + attributes: { name: 'Important' } + }, + { + type: 'blogs', + id: => '123', + attributes: { name: 'Updated!' } + } + ] +} +``` + +## Validation Errors {#validation-errors} + +When a persistence operation is attempted but the corresponding Resource +is invalid, the transaction will be rolled back and an [errors payload](http://jsonapi.org/format/#errors) will be returned +with a `422` response code: + +```ruby +{ + errors: [{ + code: 'unprocessable_entity', + status: '422', + title: "Validation Error", + detail: "Title can't be blank", + source: { pointer: '/data/attributes/title' }, + meta: { + attribute: :title, + message: "can't be blank", + code: :blank + } + }] +} +``` + +To get this functionality, your Model must adhere to the +[ActiveModel::Validations API](https://api.rubyonrails.org/classes/ActiveModel/Validations.html). + +You get this for free with ActiveRecord, or it can be mixed in to any +PORO: + +```ruby +class Post + include ActiveModel::Validations + validates :title, presence: true +end +``` + +Errors on associations will have a slightly expanded payload: + +```ruby +{ + errors: [{ + code: 'unprocessable_entity', + status: '422', + title: 'Validation Error', + detail: "Name can't be blank", + source: { pointer: '/data/attributes/name' }, + meta: { + relationship: { + attribute: :name, + message: "can't be blank", + code: :blank, + name: :pets, + id: '444', + type: 'pets' + } + } + }] +} +``` + +When [Sideposting](#sideposting), the errors payload will contain all +invalid Resources in the graph. + +## Read on Write {#read-on-write} + +By default, the response of a persistence operation will mirror your +request. But sometimes you need control over the response. The most +common scenario is sideloading an additional entity - imagine creating +an order, and wanting the order's shipping information to come back in +the response. + +You can do this by POSTing the payload as normal, but adding query +parameters to the URL: + +```ruby +# POST /api/v1/orders?include=shipping_information + +{ + type: 'orders', + attributes: { ... } +} +``` + +This will sideload the shipping information in the response. When using +[Spraypaint](/js/), do this with: + +```typescript +order.save({ returnScope: Order.includes('shipping_information') }) +``` diff --git a/docs/concepts/relationships.md b/docs/concepts/relationships.md new file mode 100644 index 00000000..7c32aedb --- /dev/null +++ b/docs/concepts/relationships.md @@ -0,0 +1,628 @@ +--- +title: 'Relationships' +--- + +# Relationships {#relationships} + +Resources rarely stand alone. This page covers how to connect them together for sideloading, sideposting, and links. + +Resources can connect to other Resources via **relationships**. +Each relationship determines behavior for: + +* Sideloading (load both Resources in a single request) +* Links (URL to lazy-load in separate request) +* Sideposting (save both in single request) + +When connecting resources, you can imagine the logic similar to +`ActiveRecord`'s `.includes`: + +```ruby +class PostResource < ApplicationResource + has_many :comments +end + +class CommentResource < ApplicationResource + attribute :post_id, :integer, only: [:filterable] + belongs_to :post +end + +PostResource.all(include: 'comments') +# Under the hood: +# CommentResource.all(filter: { post_id: array_of_post_ids }) + +CommentResource.all(include: 'post') +# Under the hood: +# PostResource.all(filter: { id: array_of_comment_ids }) +``` + +> Note the explicit `post_id` filter on `CommentResource` + +## Deep Queries {#deep-queries} + +A query that applies to a relationship is referred to as a **deep +query**. Use the dot-syntax to deep query: + +`/employees?include=positions&filter[positions.title]=Manager` + +`/employees?include=positions.department&filter[positions.department.name]=Engineering` + +The above references the **relationship name**. For simplicity, you can +also pass the JSONAPI type in brackets: + +`/employees?include=positions.department&filter[departments][name]=Engineering` + +Sorting and pagination currently only support the JSONAPI type: + +`/employees?include=positions.department&sort=departments.name` + +`/employees?include=positions.department&page[departments][size]=10` + +## Customizing Relationships {#customizing-relationships} + +The default options you can override are: + +```ruby +has_many :positions, + foreign_key: :employee_id, + primary_key: :id, + resource: EmployeeResource, + readable: true, + writable: true, + link: self.relationship_links, # the resource default mode, normally true + single: false, # only allow this sideload when one employee + resource_ids: false +``` + +`resource_ids` is the one whose default depends on the relationship type: + +| type | renders resource ids by default | +|---|---| +| `belongs_to` | yes, when its foreign key already holds the related id | +| `has_one` | no | +| `has_many` | no | +| `many_to_many` | no | +| `polymorphic_belongs_to` | no | + +`belongs_to` renders them so a client can see which record a relationship points at without following the link: + +```json +"employee": { + "data": { "type": "employees", "id": "1" }, + "links": { "related": "/employees?filter[id]=1" } +} +``` + +That costs nothing, because the id is already on the parent as its foreign key. + +No other relationship type has a free source for its ids. A collection accepts `resource_ids: true`, but that reads the association on every render of every parent record, whether or not the request wants the relationship. That is the N+1 from [#167](https://github.com/graphiti-api/graphiti/issues/167#issuecomment-686866646) on every response. Leave collections off and let clients `?include=` them. + +Not every `belongs_to` can use its foreign key. A `scope` or `params` block or a `base_scope` can filter out the record the key points at, a polymorphic target's type varies per record while rendered ids carry one type for the whole relationship, a remote resource has no local key to read, and a custom `primary_key` points the relationship at some other column. Those load the association instead, so they stay off by default too. + +
+Which `belongs_to` declarations render resource ids, and which do not + +```ruby +# yes. employee_id is the employee's id, so the payload already has it +belongs_to :employee + +# no. nothing renders at all, ids included +belongs_to :employee, readable: false + +# no. employee_id holds a name, not the related id +belongs_to :employee, primary_key: :first_name + +# no. the base scope can exclude the employee the key points at, and +# graphiti cannot know whether it does without running it +belongs_to :employee, base_scope: -> { Employee.all } + +# no. a remote resource has no local foreign key to read +belongs_to :employee, remote: "http://foo.com/employees" + +# no. the record's own class decides its type, so the key gives an id +# with no type to pair it with +belongs_to :employee, resource: CreditCardResource + +# no. the scope can exclude the employee the key points at, and graphiti +# cannot know whether it does without running it +belongs_to :employee do + scope { |ids| {type: :employees, conditions: {id: ids}} } +end + +# no. same, a params filter can exclude the employee the key points at +belongs_to :employee do + params { |hash, positions| hash[:filter][:active] = true } +end + +# no. credit_card_type is local, but rendered ids carry one type for the +# whole relationship and this one's varies per record +polymorphic_belongs_to :credit_card do + group_by(:credit_card_type) do + on(:Visa).belongs_to :visa, resource: VisaResource + end +end +``` + +Watch for the `scope`, `params` and `base_scope` cases. Nothing about those declarations looks like it concerns resource ids, so adding a scope block to filter a relationship also stops its ids from rendering. + +If you keep a `schema.json`, the schema check catches that. A relationship that renders resource ids is marked `linkage: true`, and one that stops rendering them is reported as a breaking change. Gaining them is additive and passes. + +To render ids anyway, opt in on the relationship and accept the query: + +```ruby +belongs_to :employee, resource_ids: true do + scope { |ids| {type: :employees, conditions: {id: ids}} } +end +``` + +Know what that buys for the `scope`, `params` and `base_scope` cases. Rendering reads the association off the model, which does not apply the block, so if the block narrows what sideloading returns, the ids will disagree with it. Opting in there says you know the two agree. A `primary_key`, polymorphic or remote relationship does resolve to the right id this way. + +
+ +### belongs_to_resource_ids_by_default {#belongs-to-resource-ids} + +To change how far a `belongs_to` goes, across a whole API, set it on the resource everything inherits from: + +```ruby +class ApplicationResource < Graphiti::Resource + self.belongs_to_resource_ids_by_default = :foreign_key +end +``` + +| | | +|---|---| +| `:foreign_key` | Default. Render resource ids wherever the foreign key already holds the related id, and never run an extra query. | +| `:always` | Render them for every `belongs_to`, loading the association when the foreign key cannot answer. A query per record, per relationship, on every render. | +| `:never` | Render none. This is the 1.x payload. | + +Subclasses inherit it, and a relationship passing `resource_ids` explicitly still wins. + +All three describe requests that do not include the relationship. A relationship the request does include renders its ids whatever this is set to, `:never` included, because the records are already loaded and sitting in `included`. + +Before flipping the setting, [`bin/rake graphiti:audit`](/topics/debugging#graphiti-audit) reports how every relationship renders resource ids today and which would start loading the association. + +#### What a client sees {#relationship-payload-shapes} + +A client never has to work out which rule applied. The relationship object says what it knows: + +```json +"employee": { "data": { "type": "employees", "id": "1" } } // here is the id +"employee": { "links": { "related": "..." } } // fetch it yourself +``` + +A relationship with neither is left out of the payload. Relationships are linked by default, so the link shape is the one you normally see. + +`self.relationship_placeholders = true` brings back the 1.x shape, `{"meta": {"included": false}}`. It is not part of JSON:API and carries nothing a client can act on. + +The setting covers `belongs_to` and `polymorphic_belongs_to`, and no collection, deliberately. An API-wide `:always` on collections would be the N+1 from [#167](https://github.com/graphiti-api/graphiti/issues/167#issuecomment-686866646) applied everywhere at once. + +`:always` renders ids by loading the association, so a relationship naming a method the model does not have raises on every render once you set it. + +### Conditional Relationships {#conditional-relationships} + +Like attributes, the `readable` and `writable` flags on a relationship accept more than a boolean: pass a symbol, string, or proc and the relationship becomes conditional, evaluated per-request. + +```ruby +class EmployeeResource < ApplicationResource + has_many :salary_histories, readable: :admin?, writable: :admin? + + def admin? + context.current_user.admin? + end +end +``` + +When a readable guard returns `false`, the relationship is omitted from the serialized output and any attempt to sideload it via `?include=` is silently scrubbed from the request. When a writable guard returns `false`, sideposting to that relationship is rejected with an `unwritable_relationship` validation error. + +Unlike attribute guards, relationship guards take no arguments. Include scrubbing happens before any records have been fetched, so there is no model to hand them. Base the decision on `context` alone. + +The guard can live on either side of the relationship. Graphiti first looks for the method on the resource declaring the relationship. If it isn't defined there but is defined on the related resource, the related resource's method is used. Defining the guard on the related resource lets a single guard cover every relationship pointing at it: + +```ruby +class SalaryHistoryResource < ApplicationResource + # Any resource declaring a relationship to SalaryHistoryResource with + # readable: :admin? will use this method, unless it defines its own. + def admin? + context.current_user.admin? + end +end +``` + +> **Upgrading to 1.12:** relationship guards are new enforcement, not a new +> option. Before 1.12, a symbol, string, or proc passed to a relationship's +> `readable`/`writable` was accepted and silently treated as `true`. The guard +> was never called. Those guards now run. If your app already passes one of +> these, a relationship that has been serialized all along may start +> disappearing from responses. +> +> To list every guarded relationship in your app before deploying, run +> `bin/rails runner 'puts Graphiti.guarded_relationships'`. +> +> Apps using `schema.json` also get this for free: guarded relationships are +> flagged in the schema, and the schema check reports them as +> `became guarded`. + +### Customizing Scope {#customizing-scope} + +Use `params` to change the query parameters that will be passed to the +associated Resource: + +```ruby +has_many :active_positions, resource: PositionResource do + params do |hash, employees| + hash[:filter][:active] = true + end +end + +# Would cause the underlying query: +# +# PositionResource.all({ +# filter: { +# employee_id: array_of_employee_ids +# active: true +# } +# }) +``` + +If there is no existing AR association for this we would also need to make it a getter/setter on the model. + +```ruby +# app/models/position.rb +attr_accessor :active_positions +``` + +### Customizing Assignment {#customizing-assignment} + +Once we've fetched primary data and its relationship (e.g. we have an +`employees` array and `positions` array), we need to associate these +objects: + +```ruby +employees.each do |e| + e.positions = positions.select { |p| p.employee_id == e.id } +end +``` + +Occasionally this logic will be non-standard or more complex. Use +`assign_each` to customize, returning all relevant children for the +given parent: + +```ruby +has_many :positions do + assign_each do |employee, positions| + positions.select { |p| p.belongs_to?(employee) } + end +end +``` + +Or if all else fails, use `#assign` to control all the logic: + +```ruby +has_many :positions do + assign do |employees, positions| + employees.each do |employee| + positions.select { |p| p.belongs_to?(employee) } + end + end +end +``` + +**Note**: ActiveRecord will sometimes cause unexpected queries when +assigning. If you're overriding `#assign`, make sure to keep an eye on this. If using `#assign_each`, you're fine because the adapter will take +care of this for you. + +## has_many {#has-many} + +```ruby +has_many :positions +``` + +Defaults to these common options: + +```ruby +has_many :positions, + foreign_key: :employee_id, + primary_key: :id, + resource_ids: false, + resource: PositionResource +``` + +Which would cause the following query when sideloading: + +```ruby +PositionResource.all({ filter: { employee_id => employee_ids } }) +``` + +This means **we need to make sure that filter is supported**: + +```ruby +class PositionResource < ApplicationResource + attribute :employee_id, :integer, only: [:filterable] + # ... code ... +end +``` + +Once we've resolved `employees` and `positions` the resulting objects +would be associated with logic similar to: + +```ruby +employees.each do |e| + e.positions = positions.select { |p| p.employee_id == e.id } +end +``` + +And generate a Link: + +`/positions?filter[employee_id]=1,2,3` + +## belongs_to {#belongs-to} + +```ruby +belongs_to :employee +``` + +Defaults to these common options: + +```ruby +belongs_to :employee, + foreign_key: :employee_id, + primary_key: :id, + resource_ids: true, + resource: EmployeeResource +``` + +Which would cause the following query when sideloading: + +```ruby +EmployeeResource.all({ filter: { id => position_ids } }) +``` + +And assign the resulting objects with logic similar to: + +```ruby +positions.each do |p| + p.employee = employees.find { |e| p.employee_id == e.id } +end +``` + +And generate a Link: + +`/employees?filter[id]=1,2,3` + +## has_one {#has-one} + +`has_one` works exactly like `has_many`, but only one record will be +returned. When sideloading this will be a single element, much like +`belongs_to`. + +There is one small caveat: Links always point to an `index` action, so we can apply filters. That means following *`has_one` Link will lead to +an array*, and you should select the first record. + +### Faux has_one {#faux-has-one} + +A "Faux Has One" occurs when there is more than one record of +associated data, but we only want to return the *first* record in that +array. Consider this `ActiveRecord` relationship: + +```ruby +# app/models/employee.rb +has_many :positions +has_one :current_position, -> { where(created_at: :desc) }, class_name: 'Position' + +Employee.includes('current_position').to_a + +# SELECT * FROM employees +# SELECT * FROM positions WHERE employee_id IN (?) ORDER BY created_at DESC +``` + +When we eager load, *more than one Position is returned from the +database query*. Assigning only the first record and dropping the rest +occurs in ruby, not the database query. + +The same thing happens in Graphiti: + +```ruby +# app/resources/employee_resource.rb +has_many :positions +has_one :current_position, resource: PositionResource do + params do |hash| + hash[:sort] = '-created_at' + end +end + +EmployeeResource.all(include: 'current_position') +# PositionResource.all({ +# filter: { employee_id: employee_ids }, +# sort: '-created_at' +# }) +``` + +Though everything works as expected, a large number of Position records +can incur a performance penalty (as we'd be instantiating a large number +of ActiveRecord objects). + +For this reason, you are encouraged to model Faux Has One's in such a +way that the underlying database query only returns the relevant single +record. Imagine if we had a `historical_index` column on `positions`, where a value of `1` meant "most recent": + +```ruby +# app/models/employee.rb +has_many :positions +has_one :current_position, -> { where(historical_index: 1) }, class_name: 'Position' + +Employee.includes('current_position').to_a + +# SELECT * FROM employees +# SELECT * FROM positions WHERE employee_id IN (?) AND historical_index = 1 +``` + +We've ensured the *query itself* only returns a single record. +Optimizing a Graphiti API is the same as optimizing queries. + +## many_to_many {#many-to-many} + +> This relationship is specific to relational databases that use a "join +> table" between two tables. + +Though you can make this work for other ORMs/clients, it's easiest to +explain by focusing on `ActiveRecord`. + +First, **you must use [has_many :through](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association) and not has_and_belongs_to_many**: + +```ruby +class Employee < ApplicationRecord + has_many :team_memberships + has_many :teams, through: :team_memberships +end + +class TeamMembership < ApplicationRecord + belongs_to :employee + belongs_to :team +end + +class Team < ApplicationRecord + has_many :team_memberships + has_many :employees, through: :team_memberships +end +``` + +You can always expose `team_memberships` to your API - particularly +useful if that table holds metadata about the relationship. + +Other times, however, clients of the API should not have knowledge of +this implementation detail. In these cases, use `many_to_many`: + +```ruby +class EmployeeResource < ApplicationResource + many_to_many :teams +end +# Generates the Link +# /teams?filter[employee_id]=1,2,3 + +class TeamResource < ApplicationResource + many_to_many :employees +end +# Generates the Link +# /employees?filter[team_id]=1,2,3 +``` + +The `many_to_many` call will automatically add a Filter to the associated resource. The logic for that filter, in the case of `ActiveRecord`: + +```ruby +# app/resources/team_resource.rb + +filter :employee_id, :integer do + eq do |scope, value| + scope + .includes(:team_memberships) + .where(team_memberships: { employee_id: value }) + end +end +``` + +To customize the foreign key, you will need to specify a hash rather +than a symbol. The hash key is the join association name, so the above is +equivalent to + +```ruby +# app/resources/employee_resource.rb + +many_to_many :teams, foreign_key: { team_memberships: :employee_id } +``` + +If using ActiveRecord, and the API relationship name does not match your +Model relationship name, use `:as` to specify the model relationship +that should be used to derive the query: + +```ruby +# The API relationship is "teams", ActiveRecord has "groups" +many_to_many :teams, as: :groups +``` + +## polymorphic_belongs_to {#polymorphic-belongs-to} + +With polymorphic associations, a Resource can belong to more than one other Resource, on a single association. Though these relationships are not specific to `ActiveRecord`, we'll use `ActiveRecord` conventions to describe the use case. + +Given the following [polymorphic ActiveRecords](https://guides.rubyonrails.org/association_basics.html#polymorphic-associations): + +```ruby +class Note < ApplicationRecord + belongs_to :notable, polymorphic: true +end + +class Employee < ApplicationRecord + has_many :notes, as: :notable +end + +class Department < ApplicationRecord + has_many :notes, as: :notable +end + +class Team < ApplicationRecord + has_many :notes, as: :notable +end +``` + +By `ActiveRecord` convention, the `notes` table would have columns `notable_id` and `notable_type`. + +Graphiti has the same concept. In this case we would group all the notes +by a given `notable_type`, and follow a different `belongs_to` +association for each group: + +```ruby +# app/resources/note_resource.rb +polymorphic_belongs_to :notable do + group_by(:notable_type) do + on(:Employee) + on(:Department) + on(:Team) + end +end +``` + +The `on` DSL is shorthand for a `belongs_to` relationship that accepts +all the usual options and customizations: + +```ruby +on(:Employee).belongs_to :employee, + resource: EmployeeResource + # ... etc ... +``` + +In other words: group all Notes by `notable_type`, and for all that have the value of `"Employee"` use the `belongs_to :employee` relationship +for further querying. + +## polymorphic_has_many {#polymorphic-has-many} + +Continuing from the prior section, the corresponding association of a +`polymorphic_belongs_to` is a `polymorphic_has_many`: + +```ruby +class EmployeeResource < ApplicationResource + polymorphic_has_many :notes, as: :notable +end +``` + +Predictably, this causes the query: + +```ruby +NoteResource.all({ + filter: { + notable_type: 'Employee', + notable_id: employee_ids + } +}) +``` + +And the Link + +`/notes?filter[notable_id]=1,2,3&filter[notable_type]=Employee` + +Which means the following filters are required: + +```ruby +class NoteResource < ApplicationResource + attribute :notable_id, :integer, only: [:filterable] + attribute :notable_type, :string, only: [:filterable] + # ... code ... +end +``` diff --git a/docs/concepts/resources.md b/docs/concepts/resources.md new file mode 100644 index 00000000..dd81043b --- /dev/null +++ b/docs/concepts/resources.md @@ -0,0 +1,769 @@ +--- +title: 'Resources' +--- + +# Resources + +A Resource is an abstraction around an API endpoint, the way a Model is an abstraction around a database table. It holds the logic for **querying**, **persisting**, and **serializing** one kind of thing. + +```ruby +class EmployeeResource < ApplicationResource + attribute :first_name, :string + attribute :age, :integer + + has_many :positions +end +``` + +This page is the full reference. For the whole API on one screen, see the [cheatsheet on the home page](/). For how a request flows through a Resource, see [Lifecycle of a Request](/concepts/overview). + +Resources connect to each other. That's covered separately in [Relationships](/concepts/relationships), and writes in [Persisting](/concepts/persisting). + +## Attributes {#attributes} + +```ruby +attribute :first_name, :string +``` + +A **name** (`first_name`) maps to a JSON key. A **Type** (`string`) maps to a JSON value and its coercion rules. + +### Limiting Behavior {#limiting-behavior} + +```ruby +attribute :name, :string, + readable: true, # renders in responses + writable: true, # accepted on create/update + sortable: true, # ?sort=name works + filterable: true, # ?filter[name]=... works + schema: true # exported to schema.json, not affected by only/except +``` + +Turn any flag off directly, or with `only`/`except` shorthand: + +```ruby +attribute :name, :string, sortable: false +attribute :name, :string, only: [:sortable] +attribute :name, :string, except: [:writable] +``` + +**Guards.** `readable` and `writable` also accept a symbol, string, or proc. The behavior applies only when the guard returns `true`, and the guard's arity decides what it receives: + +```ruby +attribute :name, :string, writable: :admin? +attribute :salary, :integer, readable: :visible?, writable: :salary_writable? + +def admin? # no arguments + context.current_user.admin? +end + +def visible?(model) # the model + model.internal == false +end + +def salary_writable?(model, attribute_name) # the model and the attribute name + PolicyChecker.new(model).attribute_writable?(attribute_name) +end +``` + +The model is only looked up when a guard declares a parameter for it, so zero-argument guards cost nothing. On an update it's the persisted record. On a create, it's a new unsaved instance. + +| Guard returns `false` on | Result | +| --- | --- | +| `readable` | The attribute is omitted from the response. | +| `writable` | The request is rejected with an `unwritable_attribute` validation error, before anything is persisted. | + +### Default Behavior {#default-behavior} + +```ruby +# On ApplicationResource, affects every subclass +self.attributes_readable_by_default = false # default true +self.attributes_writable_by_default = false # default true +self.attributes_filterable_by_default = false # default true +self.attributes_sortable_by_default = false # default true +self.attributes_schema_by_default = false # default true +``` + +Each `*_by_default` setting can also be a guard symbol, delegating the check to a method. Useful for wiring every attribute through one authorization system: + +```ruby +self.attributes_readable_by_default = :attribute_readable? + +def attribute_readable?(model_instance, attribute_name) + PolicyChecker.new(model_instance).attribute_readable?(attribute_name) +end +``` + +### Customizing Display {#customizing-display} + +```ruby +attribute :name, :string do + @object.name.upcase # @object is the model instance +end +``` + +### Types {#types} + +| Type | Notes | +| --- | --- | +| `string` | | +| `integer` | | +| `integer_id` | Renders as a string, queries/persists as an integer. Default type for `id`. | +| `uuid` | Like `string`, but only `eq`/`not_eq`, case-sensitive by default. | +| `string_enum` | Like `string`, but only `eq`/`not_eq`/`eql`/`not_eql`, and requires `allow:`. | +| `integer_enum` | Like `integer`, but only `eq`/`not_eq`, and requires `allow:`. | +| `big_decimal` | | +| `float` | | +| `boolean` | | +| `date` | | +| `datetime` | | +| `hash` | | +| `array` | | + +Every type except `boolean`, `hash`, and `array` also has an `array_of_*` variant: `array_of_integers`, `array_of_dates`, `array_of_uuids`, and so on. + +Each Type governs reading, writing, and filtering by wrapping a [Dry Type](https://dry-rb.org/gems/dry-types). Inspect one to see its parts: + +```ruby +Graphiti::Types[:integer_id] + +# { +# params: Dry::Types['coercible.integer'], +# read: Dry::Types['coercible.string'], +# write: Dry::Types['coercible.integer'], +# ... +# } +``` + +Edit an implementation in place. Here, `:string` is made to render as an integer: + +```ruby +Graphiti::Types[:string][:read] = Dry::Types['coercible.integer'] +``` + +#### Disabling Read Typecasting {#typecast-reads} + +To serialize values exactly as the model returns them, skipping the type's `read` coercion: + +```ruby +class ApplicationResource < Graphiti::Resource + self.typecast_reads = false +end +``` + +Defaults to `true`. Like the other class attributes it inherits, so it can be turned off app-wide or per resource. Writes and filters still coerce. + +#### Enum Types {#enum-types} + +`string_enum` and `integer_enum` behave like `string` and `integer`, except declaring one (as an attribute or a filter) requires the `allow:` option, the list of acceptable values: + +```ruby +attribute :status, :string_enum, allow: ['draft', 'published'] +``` + +If your attribute is backed by an ActiveRecord enum, reference the values directly: + +```ruby +# app/models/post.rb +class Post < ApplicationRecord + enum status: { + draft: 0, + published: 1 + } +end + +# app/resources/post_resource.rb +class PostResource < ApplicationResource + attribute :status, :string_enum, allow: Post.statuses.keys +end +``` + +See [Filter Options](#filter-options) for more on `allow`. + +Graphiti does not validate enum values on write. Your model layer is still expected to validate incoming data. + +#### Custom Types {#custom-types} + +[Dry Types supports custom types](https://dry-rb.org/gems/dry-types/main/custom-types/): + +```ruby +# Define the Type +definition = Dry::Types::Nominal.new(String) +type = definition.constructor do |input| + input.upcase +end + +# Register it with Graphiti +Graphiti::Types[:caps_lock] = { + params: type, + read: type, + write: type, + kind: 'scalar', + canonical_name: :caps_lock, + description: 'All capital letters' +} + +# Use in a Resource +attribute :name, :caps_lock +``` + +## Querying {#querying} + +```ruby +class PostResource < ApplicationResource + # Applies to every query: start with a base scope, alter it based on + # the incoming request. Called just like ActiveRecord's Post.all. + def base_scope + Post.all + end + + # Must execute the query and return an array of Model instances. + def resolve(scope) + scope.to_a + end +end +``` + +### Query Interface {#query-interface} + +Resources can query and persist without an API request or response. Pass a [JSONAPI-compliant](http://jsonapi.org) query hash directly: + +```ruby +EmployeeResource.all({ + filter: { first_name: 'Jane' }, + sort: '-created_at', + page: { size: 10, number: 2 } +}) +``` + +The return value from `.all` is a **proxy** object, similar to `ActiveRecord::Relation`. No query fires until you call `.map`, `.data`, or a render method: + +```ruby +employees = EmployeeResource.all +employees.class # Graphiti::ResourceProxy +employees.map(&:first_name) # => ["Jane", "Joe", ...] +employees.data # => [#, #, ...] + +employees.to_jsonapi +employees.to_json +employees.to_xml +``` + +`.find` returns a single record's proxy by id, raising `Graphiti::Errors::RecordNotFound` if none are returned: + +```ruby +employee = EmployeeResource.find(id: 123) +employee.data.first_name # => "Jane" +``` + +### Composing with Scopes {#composing-with-scopes} + +#### #base_scope {#base-scope} + +```ruby +def base_scope + Position.where(active: true) +end +``` + +Override `#base_scope` for logic that should apply to every query. Here, it only ever returns active Positions. + +Pass a second argument to `.all` to override the base scope for a single call: + +```ruby +class InactivePostsController < PostsController + def index + posts = PostResource.all(params, Post.where(active: false)) + render jsonapi: posts + end +end +``` + +### Sort {#sort} + +```ruby +sort :name, :string do |scope, direction| + scope.order(first_name: direction, last_name: direction) +end +``` + +Omit the type if a matching `attribute` is already defined. This overrides its default sort behavior: + +```ruby +attribute :name, :string + +sort :name do |scope, direction| + # ... code ... +end +``` + +`sort` on its own defines a sort-only attribute. Define the `attribute` first if you also need filtering or other behavior. + +#### Sort Options {#sort-options} + +| Option | Description | +| --- | --- | +| `only` | Restrict to a single direction, e.g. `sort :name, only: [:desc]` | + +### Filter {#filter} + +```ruby +filter :name, :string do + eq do |scope, value| + scope.where(first_name: value) + end + + # prefix do ... end + # suffix do ... end + # etc +end +``` + +Omit the type if a matching `attribute` is already defined. This overrides its default filter behavior. `filter` on its own defines a filter-only attribute. Define the `attribute` first if you also need sorting or other behavior. + +Every operator below also has a `not_` counterpart (`not_eq`, `not_prefix`, ...). Values arrive as an array unless the filter is `single: true`. Comma-delimit multiple values in a query string (`/employees?filter[name]=Jane,John`). + +| Type | Default operators | +| --- | --- | +| `string` | `eq`, `eql`, `prefix`, `suffix`, `match` | +| `uuid` | `eq` | +| `string_enum`, `integer_enum` | `eq`, `eql` | +| `integer_id`, `integer`, `big_decimal`, `float`, `date`, `datetime` | `eq`, `gt`, `gte`, `lt`, `lte` | +| `boolean` | `eq` (always `single: true`) | +| `hash` | `eq` | +| `array` | `eq` | + +Define custom operators on the fly: + +```ruby +filter :name do + fuzzy_match do |scope, value| + # ... code ... + end +end +``` + +This supports `filter[name][fuzzy_match]=foo`. + +#### Filter Options {#filter-options} + +| Option | Description | +| --- | --- | +| `only`, `except` | Limit the operators generated from the type's defaults, e.g. `filter :name, :string, only: [:eq, :suffix]` | +| `allow` | Only permit these values, e.g. `filter :size, :string, allow: ['Big', 'Medium', 'Small']` | +| `deny` | Reject these values, e.g. `filter :size, :string, deny: ['X-Large']` | +| `single` | Accept one value instead of an array. `boolean` filters are `single: true` by default. | +| `required` | Reject the request if the filter is absent, e.g. `filter :customer_id, :string, required: true` (equivalently, `attribute :customer_id, :integer, filterable: :required`) | +| `dependent` | Require other filters alongside this one, e.g. `filter :customer_id, :integer, dependent: [:customer_type]` paired with `filter :customer_type, :string, dependent: [:customer_id]`, so querying by id requires type, and vice versa | +| `blanks` | What to do with a blank value. `:literal` (default) takes `"null"` and `""` as strings, `:null` coerces `"null"` to Ruby `nil` so the filter can query for NULL, and `:rejected` raises `InvalidFilterValue` for `nil`, `""`, `[]` or `"null"`. Set `self.filter_blanks_treated_as` on a Resource to change it for all of that Resource's filters. | + +```ruby +# Default behavior +filter :name, :string do + eq do |scope, value| + value # => ["Jane"] + end +end + +# With single: true +filter :name, :string, single: true do + eq do |scope, value| + value # => "Jane" + end +end +``` + +#### Boolean Filter {#boolean-filter} + +Filters with type `boolean` are `single: true` by default. A boolean filter accepting multiple values doesn't make sense. + +#### Hash Filter {#hash-filter} + +Filters with type `hash` parse JSON automatically when passed in a URL query string: + +```ruby +# GET /employees?filter[metadata]={ "foo": 100 } + +filter :metadata, :hash do + eq do |scope, value| + value # => [{ "foo" => 100 }] + end +end +``` + +#### Escaping Values {#escaping-values} + +By default, Graphiti parses a comma-delimited string as an array. Wrap a value in `{{curlies}}` to keep it intact, for a "keyword search" field that could itself contain a comma: + +```ruby +# GET /employees?filter[keywords]={{some,value}} + +filter :keywords, :string do + eq do |scope, value| + value # => "some,value" + end +end +``` + +Or define an array explicitly instead of relying on comma-splitting: + +```ruby +# GET /employees?filter[keywords]=[some,value] + +filter :keywords, :string do + eq do |scope, value| + value # => ["some", "value"] + end +end +``` + +A `single: true` filter skips array parsing entirely and escapes the value for you, filtering on the string as given. + +### Pagination {#pagination} + +Collections are paginated by default, 20 records to a page: + +```ruby +PostResource.all({ page: { number: 2, size: 10 } }) +# GET /posts?page[number]=2&page[size]=10 +``` + +Two settings you might want to adjust, both usually set on `ApplicationResource`: + +```ruby +class ApplicationResource < Graphiti::Resource + self.page_default_size = 10 # unset falls back to 20 + self.page_max_size = 100 # default 1_000 +end +``` + +A request asking for more than `page_max_size` raises `Graphiti::Errors::UnsupportedPageSize` rather than quietly serving it. + +A sideload can be paginated only when there is a single parent record: + +``` +/employees/1?include=positions&page[positions][size]=2 # fine +/employees?include=positions&page[positions][size]=2 # raises UnsupportedPagination +``` + +The second asks for two positions per employee, and one query with one `LIMIT` can only cap the whole result, not each employee's share of it. ActiveRecord has the same limitation. Where you need the shape anyway, a named relationship such as `has_one :top_position` gets you one row per parent. + +To paginate a scope your adapter cannot handle, override it on the Resource: + +```ruby +paginate do |scope, current_page, per_page, offset| + scope.by_page(current_page, per_page) +end +``` + +#### Cursors {#pagination-cursors} + +`page_cursors` renders a cursor in every record's `meta`, and the client pages by handing one back: + +```ruby +class PostResource < ApplicationResource + self.page_cursors = true # default false +end +``` + +```json +{ + "id": "42", + "type": "posts", + "attributes": { "title": "Hello" }, + "meta": { "cursor": "eyJvZmZzZXQiOjQxfQ==" } +} +``` + +```ruby +PostResource.all({ page: { after: "eyJvZmZzZXQiOjQxfQ==", size: 10 } }) +# GET /posts?page[after]=eyJvZmZzZXQiOjQxfQ%3D%3D&page[size]=10 +``` + +`page[before]` walks backwards from a cursor. It cannot be combined with `page[number]`, which raises `Graphiti::Errors::UnsupportedBeforeCursor`. + +The links a client follows to page are separate, and covered in [Pagination Links](/concepts/links#pagination-links). + +### Statistics {#statistics} + +```ruby +stat total: [:count] +stat rating: [:average] +stat likes: [:sum] +stat score: [:maximum] + +stat rating: [:average] do + standard_deviation do |scope, attr| + # your standard deviation code here + end +end +``` + +Every Resource has a `total: :count` statistic by default. Statistics respect filtering but not pagination, so you can show a "Total Posts" count above a paginated grid without a second request: + +```ruby +PostResource.all({ + stats: { total: 'count' } +}) +# GET /posts?stats[total]=count +``` + +```ruby +{ + meta: { + stats: { + total: { + count: 100 + } + } + } +} +``` + +### Extra Fields {#extra-fields} + +```ruby +extra_attribute :net_worth +``` + +Works like `attribute`, except the field is read-only and only returned when explicitly requested: `?extra_fields[employees]=net_worth`. + +Adjust the scope (e.g. to eager-load) only when the extra field is requested: + +```ruby +resource.on_extra_attribute :net_worth do |scope| + scope.includes(:assets) +end +``` + +### #resolve {#resolve} + +`#resolve` must execute the query and return an array of `Model` instances. Override it to add behavior around the default: + +```ruby +def resolve(scope) + Rails.logger.info "begin resolving scope..." + result = super + Rails.logger.info "resolved!" + result +end +``` + +## Configuration {#configuration} + +```ruby +class PostResource < ApplicationResource + self.model = Post + self.type = 'posts' + + # Only used if you care about Links + primary_endpoint '/posts', [:index, :show, :create, :update, :destroy] + + self.default_sort = [{ title: :asc }] # default nil + self.page_default_size = 10 # default 20 +end +``` + +Typically inherited from `ApplicationResource`, where cross-cutting settings live: + +```ruby +class ApplicationResource < Graphiti::Resource + # Required when there's no corresponding model + self.abstract_class = true + + # Subclasses override as needed + self.adapter = Graphiti::Adapters::ActiveRecord + + # Default attribute flags. See #limiting-behavior + self.attributes_readable_by_default = true + self.attributes_writable_by_default = true + self.attributes_sortable_by_default = true + self.attributes_filterable_by_default = true + + # Used for link generation + self.base_url = ENV.fetch('BASE_URL', 'http://localhost:3000') + # Suggest referencing this in config/routes.rb: + # scope path: '/api/v1' do + # resources :posts + # end + self.endpoint_namespace = '/api/v1' + + # Refuse requests reaching this Resource from a URL it isn't allowlisted for + self.validate_requests = true + + # Refuse to render a link pointing at an endpoint that isn't routable + self.validate_links = true + + # Render relationship links: true, false, or :on_demand + self.relationship_links = true +end +``` + +### Polymorphic Resources {#polymorphic-resources} + +Polymorphic Resources are similar to [ActiveRecord STI](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html): a single query returns multiple Resource types. Querying `/tasks` can return `bugs`, `features`, and `epics`. + +```ruby +class Employee < ApplicationRecord + has_many :tasks +end + +# tasks table has a 'type' column +class Task < ApplicationRecord + belongs_to :employee +end + +class Bug < Task +end + +# ONLY Feature has #points +class Feature < Task + def points + 5 + end +end + +# ONLY Epic has the milestones relationship +class Epic < Task + has_many :milestones +end + +class Milestone < ApplicationRecord + belongs_to :epic +end +``` + +```ruby +class TaskResource < ApplicationResource + # Reference child classes + self.polymorphic = [ + 'BugResource', + 'FeatureResource', + 'EpicResource' + ] + + attribute :title, :string +end + +class BugResource < TaskResource +end + +class FeatureResource < TaskResource + attribute :points, :integer +end + +class EpicResource < TaskResource + has_many :milestones +end + +class MilestoneResource < TaskResource + belongs_to :epic +end +``` + +`/tasks` returns [JSONAPI types](http://jsonapi.org/format/#document-resource-identifier-objects) of `bugs`, `features`, and `epics`. Only `features` render `points`. Only `epics` render the `milestones` relationship. `/tasks?include=milestones` correctly only queries and renders Milestones for Epics. + +Resources connect to each other through relationships. See [Relationships](/concepts/relationships). + +## Generators {#generators} + +```bash +$ rails generate graphiti:resource NAME [attribute:type] [options] +``` + +```bash +$ rails generate graphiti:resource Employee first_name:string age:integer +``` + +Adds a route, controller, resource, and tests. + +Limit the actions the resource supports with `-a`: + +```bash +$ rails generate graphiti:resource Employee -a index show +``` + +Writing data (creating, updating, and destroying resources, including a graph of them in a single request) is covered in [Persisting](/concepts/persisting). + +## Context {#context} + +```ruby +# app/resources/post_resource.rb +attribute :active, :boolean, writable: :admin? + +def admin? + context.current_user.admin? +end +``` + +Every Resource has access to `#context`. Under Rails, `context` is the controller instance processing the request. + +Put common helpers like `current_user` on `ApplicationResource`, so every Resource can call them: + +```ruby +# app/resources/application_resource.rb +class ApplicationResource < Graphiti::Resource + # ... code ... + def current_user + context.current_user + end +end + +# app/resources/post_resource.rb +class PostResource < ApplicationResource + # ... code ... + def admin? + current_user.admin? + end +end +``` + +Set context manually with `with_context`: + +```ruby +ctx = OpenStruct.new(current_user: User.first) +Graphiti.with_context(ctx) do + # current_user == ctx.current_user + PostResource.all +end +``` + +## Concurrency {#concurrency} + +Under Rails, concurrency turns on by default when `::Rails.application.config.cache_classes` is `true` (the default for staging and production). Sibling sideloads then load concurrently, so a `Post` sideloading `Comments` and `Author` loads both at the same time. Your initializer runs after that default lands, so it always has the last word. That cuts both ways, since an unconditional `c.concurrency = true` forces it on everywhere, development and test included. + +```ruby +# config/initializers/graphiti.rb +Graphiti.configure do |c| + # c.concurrency = false + c.concurrency_max_threads = ENV.fetch("GRAPHITI_CONCURRENCY_MAX_THREADS", 4).to_i +end +``` + +Sideloads share a pool of `concurrency_max_threads` threads (default 4) per process. Whatever the request thread knew, the sideload knows too. `Graphiti.context`, fiber-locals and `ActiveSupport::CurrentAttributes` all carry over so `Current.user` works inside a sideload. Assignments made inside a sideload don't travel back. + +### Sizing the connection pool {#concurrency-pool-sizing} + +Every thread talking to the database holds its own connection, and concurrent sideloads are extra threads. The connection pool has to cover both. + +```yaml +# database.yml +pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5).to_i + 4 + 1 %> +``` + +That's web threads plus `concurrency_max_threads` plus a spare. Rails uses the same rule for its [async query executor](https://guides.rubyonrails.org/configuring.html#config-active-record-async-query-executor), and the default of 4 comes from there too. + +When the pool is too small you get `ActiveRecord::ConnectionTimeoutError` ("all pooled connections were in use"). It only shows up once traffic is heavy enough to drain the pool, so an undersized app can run happily for months. (So check `database.yml` and make sure `pool` reads the variable your deploys actually set.) + +The pool that drains is ActiveRecord's connection pool. Web threads and concurrent sideload threads all draw from it, which is why the formula above adds `concurrency_max_threads`. Shrinking `concurrency_max_threads` is always safe. When Graphiti's pool fills up, extra sideloads just run on the request thread on its already-counted connection, so you lose some parallelism and nothing else. Raising it is what needs care, since every sideload thread is one more claim on connections, and the formula has to grow with it. + +The database server has its own ceiling, `max_connections` in Postgres. Every Ruby process brings a full pool, so weigh that limit against your process count, meaning Puma `workers` (`WEB_CONCURRENCY`) times your server count, plus each job worker process, all multiplied by `pool`. + +`bin/rake graphiti:audit` checks the formula against this environment's numbers. + +The analysis behind these numbers is in [#469](https://github.com/graphiti-api/graphiti/issues/469), worth reading in full if you're debugging connection errors. + +## Adapters {#adapters} + +Common resource overrides can be packaged into an Adapter for code re-use, most commonly to use a different client/datastore than ActiveRecord/RelationalDB. + +[Adapters are best explained in the 'Without ActiveRecord' recipe](/topics/without-activerecord). diff --git a/docs/getting-started/first-api.md b/docs/getting-started/first-api.md new file mode 100644 index 00000000..8b8b1570 --- /dev/null +++ b/docs/getting-started/first-api.md @@ -0,0 +1,289 @@ +--- +title: 'Build Your First API' +--- + +# Build Your First API + +By the end of this page you'll have a working Rails API, backed by Graphiti, that supports filtering, sorting, pagination, and nested relationships out of the box. + +We'll use Rails and ActiveRecord here, on familiar ground. For how the pieces fit together, see [Lifecycle of a Request](/concepts/overview). + +You'll need Ruby 3.2+ and Rails 7.1+ installed for this walkthrough. Graphiti itself only requires Ruby 3.2+ and ActiveSupport, so you can [use it without Rails](/getting-started/installation#without-rails). + +## Installation {#installation} + +Let's start with a classic Rails blog. We'll use a [template](http://guides.rubyonrails.org/rails_application_templates.html) to handle some of the boilerplate. Run this command and accept all the defaults for now: + +```bash +$ rails new blog --api -m https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb +``` + +Feel free to run `git diff` if you're interested in the +particulars. This is mostly installing gems and including modules. + +> Note: if a network issue prevents you from pointing to this URL +> directly, you can download the file and and run this command as `-m +> /path/to/template` + +Alternatively, you can [**add to an existing project**](/getting-started/installation#adding-to-an-existing-app). + +## Defining a Resource {#defining-a-resource} + +A [**Resource**](/concepts/resources) defines how to query and persist your [**Model**](/concepts/backends-and-models). In other +words: a Model is to the database as Resource is to the API. So +first, let's define our Model: + +```bash +$ bundle exec rails generate model Post title:string upvotes:integer active:boolean +$ bundle exec rails db:migrate +``` + +Now we can use the built-in [generator](/concepts/resources#generators) to define our Resource, +corresponding [**Endpoint**](/concepts/endpoints), and +[**Integration Tests**](/topics/testing). + +```bash +$ bundle exec rails g graphiti:resource Post title:string upvotes:integer active:boolean +``` + +You'll see a number of files created. Now run your app!: + +```bash +$ bundle exec rails s +``` + +Verify `http://localhost:3000/api/v1/posts` renders JSON correctly. +Now we need data. + +##### Seeding Data {#seeding-data} + +Edit `db/seeds.rb` to create a few `Post`s: + +```ruby +Post.create!(title: 'My title', upvotes: 10, active: true) +Post.create!(title: 'Another title', upvotes: 20, active: false) +Post.create!(title: 'OMG! A title', upvotes: 30, active: true) +``` + +And run the script: + +```bash +$ bundle exec rails db:seed +``` + +Now load `http://localhost:3000/api/v1/posts`. You should have 3 `Post`s in +your database. + + + +
+ +## Querying {#querying} + +Now that we've defined our Resource and seeded some data, let's see +what query functionality we have. We've listed all `Post`s at `http://localhost:3000/api/v1/posts`. Let's see what we can do: + +| What you want | URL | +| --- | --- | +| Sort by title, ascending | `/api/v1/posts?sort=title` | +| Sort by title, descending | `/api/v1/posts?sort=-title` | +| Paginate, 2 per page | `/api/v1/posts?page[size]=2` | +| Paginate, 2 per page, second page | `/api/v1/posts?page[size]=2&page[number]=2` | +| Sparse fieldset: only `title` | `/api/v1/posts?fields[posts]=title` | +| Filter, simple equality | `/api/v1/posts?filter[title]=my title` | +| Filter, case-insensitive equality | `/api/v1/posts?filter[title][eql]=My title` | +| Filter, prefix | `/api/v1/posts?filter[title][prefix]=my` | +| Filter, suffix | `/api/v1/posts?filter[title][suffix]=title` | +| Filter, contains | `/api/v1/posts?filter[title][match]=itl` | +| Filter, greater than | `/api/v1/posts?filter[upvotes][gt]=20` | +| Filter, greater than or equal to | `/api/v1/posts?filter[upvotes][gte]=20` | +| Filter, less than | `/api/v1/posts?filter[upvotes][lt]=20` | +| Filter, less than or equal to | `/api/v1/posts?filter[upvotes][lte]=20` | + +Filtering on an attribute you haven't made filterable raises `Graphiti::Errors::InvalidAttributeAccess`. Filtering on one that doesn't exist raises `Graphiti::Errors::UnknownAttribute`. All filter logic can be customized, and customizations can be packaged into an **Adapter** for reuse. See [Filter](/concepts/resources#filter). + +### Extra Fields + +Some fields are expensive enough that you only want to compute them when a client asks. Declare those with `extra_attribute`: + +```ruby +# app/resources/post_resource.rb +extra_attribute :description, :string do + @object.active? ? 'Active Post' : 'Inactive Post' +end +``` + +Request it with `/api/v1/posts?extra_fields[posts]=description`. You can also eager load data conditionally when the field is requested. + +### Statistics + +Useful for search grids ("the first 10 active posts, plus the total count of all posts") in a single request. Hit `/api/v1/posts?stats[total]=count` and the result arrives in the `meta` section of the response: + +![meta_total_count](/assets/img/meta_total_count.png) + +Statistics respect your filters, so the count adjusts accordingly. There are several built-in stats and you can [add your own](/concepts/resources#statistics). + +### Error Handling + +Your app always renders a JSONAPI-compliant error response. Raise something in the controller: + +```ruby +# app/controllers/posts_controller.rb +def index + raise 'foo' +end +``` + +and this is what you'd see in production: + +![error_payload](/assets/img/error_payload.png) + +Different errors can be given different response codes, JSON, and side effects. See [Error Handling](/topics/error-handling). + +## Persisting {#persisting} + +Resources can Create, Update, and Delete (and you can persist multiple +Resources in a single request). The best way to observe this behavior is +to take a look at the tests the generator created. One example: + +```ruby +# spec/api/v1/employees/create_spec.rb +subject(:make_request) do + jsonapi_post "/api/v1/employees", payload +end + +describe 'basic create' do + let(:payload) do + { + data: { + type: 'employees', + attributes: { + first_name: 'Jane' + } + } + } + end + + it 'works' do + expect(EmployeeResource).to receive(:build).and_call_original + expect { + make_request + }.to change { Employee.count }.by(1) + expect(response.status).to eq(201) + end +end +``` + +Read more about [Persistence](/concepts/persisting) and +[Testing Persistence](/topics/testing#writes). + +## Adding Relationships {#adding-relationships} + +Let’s start by defining our Model: + +```bash +$ bundle exec rails g model Comment post_id:integer body:text active:boolean +$ bundle exec rails db:migrate +``` + +```ruby +# app/models/post.rb +has_many :comments + +# app/models/comment.rb +belongs_to :post +``` + +...and corresponding Resource object: + +```bash +$ bundle exec rails g graphiti:resource Comment body:string active:boolean created_at:datetime +``` + +Configure the relationship in `PostResource`: + +```ruby +# app/resources/post_resource.rb +has_many :comments +``` + +And allow filtering Comments based on the Post `id`: + +```ruby +# app/resources/comment_resource.rb +attribute :post_id, :integer, only: [:filterable] +``` + +This code: + +* Allows eager-loading the relationship. + * URL: `/api/v1/posts?include=comments` + * SQL: `SELECT * FROM comments WHERE post_id = 123` +* Generates a [**Link**](/concepts/links) for +lazy-loading. +* Will use `CommentResource` for querying logic (so we can [Deep +Query](/concepts/relationships#deep-queries), e.g. +"only return the latest 3 active comments"). +* By default, this will generate the query `CommentResource.all(filter: { post_id: 123 })`, but [relationships can be customized](/concepts/relationships) + +You should now be able to hit `/api/v1/comments` with all the same +functionality as before. We need to seed data. + +#### Seeding Relationships {#seeding-relationships} + +Start by clearing out your database: + +```bash +$ bundle exec rails db:migrate:reset +``` + +Replace your `db/seeds.rb` with this code to persist one `Post` and three `Comment`s: + +```ruby +comment1 = Comment.new(body: 'comment one', active: true) +comment2 = Comment.new(body: 'comment two', active: false) +comment3 = Comment.new(body: 'comment three', active: true) + +Post.create! \ + title: 'My title!', + active: true, + comments: [comment1, comment2, comment3] +``` + +And run it: + +```bash +$ bundle exec rails db:seed +``` + +## Relationship Usage {#relationship-usage} + +Now let's fetch a `Post` and filtered `Comment`s in a single request: + +`/api/v1/posts?include=comments` + +Any logic in `CommentResource` is available to us. Let's sort the comments by `created_at` descending: + +`/api/v1/posts?include=comments&sort=-comments.created_at`. + +Logic from `CommentResource` is accessible at the `/api/v1/comments` endpoint, and reusable when eager-loading Comments at `/api/v1/posts:` + +* `/api/v1/comments?filter[active]=true` +* `/api/v1/posts?include=comments&filter[comments.active]=true` + +This is why Resource objects exist: they provide an interface to +reuse code across multiple Endpoints. + +Just as we can query a graph of Resources in a single +request, we can *persist* a graph of Resources in a single request. See +[Sideposting](/concepts/persisting#sideposting). + +## Exploring with Vandal {#exploring-with-vandal} + +Graphiti ships with Vandal, a UI that introspects your schema for point-and-click data exploration. See the [Vandal Guide](/reference/vandal) to try it against this blog. + +## Next Steps {#whats-next} + +* Continue with the [Tutorial](/tutorial) for a deeper walkthrough of customization and relationships. +* Browse the [Resources guide](/) for the full capability reference. +* Read the [Testing Guide](/topics/testing) to start testing your API. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 00000000..30f526ab --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,186 @@ +--- +title: 'Installation' +--- + +:::info Requirements +Graphiti 2.0 requires Ruby 3.2+ and ActiveSupport 7.1+. Rails is optional, and 7.1+ if you use it. Coming from 1.x? Remove `graphiti-rails`, `graphiti_spec_helpers` and `graphiti_errors` from your Gemfile: they're part of the main gem now, and the [upgrade guide](/upgrading) covers the rest. +::: + +## From Scratch {#from-scratch} + +The easiest way to start from scratch is to use the application +template: + +```bash +$ rails new blog --api -m https://raw.githubusercontent.com/graphiti-api/graphiti/main/templates/rails/all.rb +``` + +Alternatively, download and point to the template locally: + +```bash +$ curl -O https://raw.githubusercontent.com/graphiti-api/graphiti/main/templates/rails/all.rb +$ rails new blog --api -m all.rb +``` + +Run `git diff` to see the changes to a blank Rails app. + +## Adding to an Existing App {#adding-to-an-existing-app} + +This process is straightforward. You can add Graphiti to an existing +Rails app alongside [JBuilder](https://github.com/rails/jbuilder) or [ActiveModelSerializers](https://github.com/rails-api/active_model_serializers). + +Start with gems: + +```ruby +# The only strictly-required gem +gem 'graphiti' + +# For automatic ActiveRecord pagination +gem 'kaminari' + +# Test-specific gems +group :development, :test do + gem 'rspec-rails' + gem 'factory_bot_rails' + gem 'faker' +end + +group :test do + gem 'database_cleaner' +end +``` + +You'll be up-and-running at this point. Verify with a simple standalone +Resource: + +```ruby +# Assuming you already have a Post ActiveRecord Model +class PostResource < Graphiti::Resource + self.adapter = Graphiti::Adapters::ActiveRecord + attribute :title, :string +end + +PostResource.all.data # => [#, #, ...] +``` + +Now we need to integrate with Rails endpoints (to give us things +like [#context](/concepts/resources#context)): + +```ruby +# app/controllers/application_controller.rb +class ApplicationController < ActionController::Base + include Graphiti::Rails::Controller +end +``` + +And wire-up our error-handling: + +```ruby +# app/controllers/application_controller.rb +# When #show action does not find record, return 404 +register_exception Graphiti::Errors::RecordNotFound, + status: 404 + +rescue_from Exception do |e| + handle_exception(e) +end +``` + +That's it for the basics. You may have issues with generators +conflicting with your existing application structure - but you can +always write files manually or [submit an issue](https://github.com/graphiti-api/graphiti/issues). + +### Responders {#responders} + +Graphiti supports JSONAPI, simple JSON, and XML. `Graphiti::Rails::Controller` carries `ActionController::MimeResponds`, so `respond_to` works even in API-only apps: + +```ruby +def index + posts = PostResource.all(params) + + respond_to do |format| + format.json { render(json: posts) } + format.jsonapi { render(jsonapi: posts) } + format.xml { render(xml: posts) } + end +end +``` + +The [Responders](https://github.com/heartcombo/responders) gem collapses that boilerplate: + +```ruby +def index + posts = PostResource.all(params) + respond_with(posts) +end +``` + +To get this functionality: + +```ruby +# Gemfile +gem 'responders' + +# app/controllers/application_controller.rb +include Graphiti::Rails::Responders +``` + +> Note: Persistence operations only support JSONAPI format, so you'll +> still use `render jsonapi:` and `render jsonapi_errors:` for those. + +### .graphiticfg.yml {#graphiticfg} + +The `.graphiticfg.yml` file lives in the root directory of your +application. It holds configuration we need to reuse across a variety of +contexts (primarily generates and rake tasks). If you use our template to create your application, it's created for you. + +Primarily this is used to hold your "API namespace": + +```yaml +namespace: /my_api/v1 +``` + +If this file doesn't exist you may get unexpected errors - make sure to +create it! + +### Testing {#testing} + +To add our [Integration Tests](/topics/testing): + +```ruby +# Gemfile +group :development, :test do + gem 'factory_bot_rails' + gem 'rspec_rails' + gem 'faker' +end + +group :test do + gem 'database_cleaner' +end +``` + +Bootstrap RSpec if you haven't already: + +```bash +$ bin/rails g rspec:install +``` + +Then add the Graphiti spec helpers and database cleaning to your `RSpec.configure` block. See [RSpec Setup](/topics/testing#rspec) in the Testing guide for the config to paste in. + +### will_paginate {#will-paginate} + +By default, we use [Kaminari](https://github.com/kaminari/kaminari) for +ActiveRecord pagination. If you prefer [will_paginate] (or anything +else): + +```ruby +# app/resources/application_resource.rb +paginate do |scope, current_page, per_page| + scope.paginate(page: current_page, per_page: per_page) +end +``` + +## Without Rails {#without-rails} + +You can use Graphiti in any plain `.rb` file, or serve it from any Rack framework. Both live in the repo's [`examples/`](https://github.com/graphiti-api/graphiti/tree/main/examples) directory: [`plain_ruby`](https://github.com/graphiti-api/graphiti/tree/main/examples/plain_ruby) is Graphiti in a single script, and [`sinatra`](https://github.com/graphiti-api/graphiti/tree/main/examples/sinatra) serves JSON:API endpoints from a Sinatra app, including error rendering via `rescue_registry`. diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 00000000..9b4e549e --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,311 @@ +--- +id: intro +title: 'Graphiti' +sidebar_label: 'Overview' +sidebar_position: 0 +slug: / +--- + +# Graphiti + +Graphiti is a serialization (and de-serialization) library for Ruby, with integrations for Rails included. + +It's built on the [JSON:API](https://jsonapi.org) spec, which settles the decisions every API accumulates: response shapes, filtering, sorting, pagination, error formats, and how related data rides along. Your client layer (often a javascript single page app) speaks this protocol in return. It isn't complicated, so client logic can be hand-rolled or you can use one of the [many available libraries](https://jsonapi.org/implementations/#client-libraries) that work with the standard. + +This is an alternative to a library like JBuilder, which builds each JSON response individually. + +Graphiti sits on top of your models and exposes them over a JSON:API-compliant interface. You define Resources instead of controllers and serializers, and get filtering, sorting, pagination, sparse fieldsets, statistics, and nested reads and writes across relationships, all over one endpoint. + +Here is the whole loop. A Resource declares what's exposed: + +```ruby title="app/resources/employee_resource.rb" +class EmployeeResource < ApplicationResource + self.model = Employee # usually inferred from the class name, here for clarity + + attribute :first_name, :string + attribute :last_name, :string + attribute :age, :integer + + has_many :positions +end +``` + +The controller hands it the request params and renders the result: + +```ruby title="app/controllers/employees_controller.rb" +class EmployeesController < ApplicationController + def index + employees = EmployeeResource.all(params) + + respond_to do |format| + format.jsonapi { render(jsonapi: employees) } + format.json { render(json: employees) } + format.xml { render(xml: employees) } + end + end + + def show + employee = EmployeeResource.find(params) + authorize employee.data # data is the Employee model, authorize is Pundit + render(jsonapi: employee) + end +end +``` + +A client asks for employees and their positions in one request: + +```http title="Request" +GET /api/v1/employees?include=positions +``` + +```json title="Response" +{ + "data": [ + { + "id": "1", + "type": "employees", + "attributes": { + "first_name": "Jane", + "last_name": "Doe", + "age": 34 + }, + "relationships": { + "positions": { + "data": [ + { "type": "positions", "id": "1" }, + { "type": "positions", "id": "2" } + ] + } + } + } + ], + "included": [ + { + "id": "1", + "type": "positions", + "attributes": { "title": "Engineer" } + }, + { + "id": "2", + "type": "positions", + "attributes": { "title": "Senior Engineer" } + } + ] +} +``` + +That same Resource also serves `?filter[age][gt]=30`, `?sort=-age`, `?page[size]=10`, `?fields[employees]=first_name`, and `?stats[total]=count`, without writing any of them. + +The same proxy renders all three formats, so `/employees.jsonapi`, `/employees.json` and `/employees.xml` all work off one action. + +`.all` and `.find` return that proxy, so nothing has been queried yet. `.data` is where you reach the model, and also where per-record authorization goes. See [Authorization](/topics/authorization#integrating-with-pundit). + +If repeating that `respond_to` block gets old, the optional [`responders`](https://github.com/heartcombo/responders) integration collapses it to `respond_with(employees)`. See [Installation](/getting-started/installation#responders). + +## The whole Resource API + +Every Resource is a collection of defaults, and you can override any of them. Below is one Resource with those defaults written out the long way, the entire surface area on a single page. You wouldn't write this much by hand. It's here so you can see what's available. + +### ApplicationResource + +Every Resource inherits from an `ApplicationResource`, the same way models inherit from `ApplicationRecord`. This is where cross-cutting configuration lives, so individual Resources stay small. It's also the right place to put helpers like `current_user`, which guards throughout your API can then call. + +```ruby title="app/resources/application_resource.rb" +class ApplicationResource < Graphiti::Resource + # Required when there's no corresponding model + self.abstract_class = true + + # Subclasses override as needed + self.adapter = Graphiti::Adapters::ActiveRecord + + # Flip any of these to lock down every Resource at once, + # e.g. a read-only API + self.attributes_readable_by_default = true + self.attributes_writable_by_default = true + self.attributes_sortable_by_default = true + self.attributes_filterable_by_default = true + + # Used for link generation + self.base_url = ENV.fetch('BASE_URL', 'http://localhost:3000') + self.endpoint_namespace = '/api/v1' + + def current_user + context.current_user + end +end +``` + +### A Resource + +An individual Resource declares its attributes and relationships, plus anything about it that differs from the defaults: + +```ruby +class EmployeeResource < ApplicationResource + # Both inferred from the class name. Set them only when they differ + self.model = Employee + self.type = :employees # the JSONAPI type + + self.default_sort = [{ name: :desc }] # default nil + self.page_default_size = 10 # default 20 + + attribute :name, :string + attribute :age, :integer + attribute :hired_at, :datetime, writable: false + + has_many :positions +end +``` + +That is a complete, working Resource. Everything below is how you override a piece of it. + +### Attributes + +```ruby +# Each flag defaults to the corresponding class-level setting +attribute :name, :string, + readable: self.attributes_readable_by_default, + writable: self.attributes_writable_by_default, + sortable: self.attributes_sortable_by_default, + filterable: self.attributes_filterable_by_default + +# Alter display +# @object is your model instance +attribute :name, :string do + @object.name.upcase +end +``` + +### Sorting + +```ruby +# Pass a type - sort :name, :string - if no attribute is defined +sort :name do |scope, dir| + scope.order(name: dir) +end +``` + +### Filtering + +```ruby +# Pass a type - filter :name, :string - if no attribute is defined +filter :name do + # All of these operators have not_ equivalents, e.g. not_eq + # Imagine ".where.not" instead of ".where" + + eq do |scope, value| + scope.where("lower(name) IN ?", value.map(&:downcase)) + end + + eql do |scope, value| + scope.where(name: value) + end + + prefix do |scope, value| + value.each do |v| + scope = scope.where('lower(name) LIKE ?', "#{v.downcase}%") + end + scope + end + + suffix do |scope, value| + value.each do |v| + scope = scope.where('lower(name) LIKE ?', "%#{v.downcase}") + end + scope + end + + match do |scope, value| + value.each do |v| + scope = scope.where('lower(name) LIKE ?', "%#{v.downcase}%") + end + scope + end +end + +# Comparison operators, for integer, float, datetime, etc +filter :age, :integer do + eq do |scope, value| + scope.where(age: value) + end + + gt do |scope, value| + value.each { |v| scope = scope.where('age > ?', v) } + scope + end + + gte do |scope, value| + value.each { |v| scope = scope.where('age >= ?', v) } + scope + end + + lt do |scope, value| + value.each { |v| scope = scope.where('age < ?', v) } + scope + end + + lte do |scope, value| + value.each { |v| scope = scope.where('age <= ?', v) } + scope + end +end +``` + +Filters receive an array of values by default, which is why each operator above iterates. Pass `single: true` to accept one value instead. + +### Querying + +```ruby +# Passed to sort, filter, paginate, etc +# Apply global logic here: only return active Employees, +# scope results to the current user, and so on +def base_scope + Employee.all +end + +# Must execute the query and return an array of Model instances +def resolve(scope) + scope.to_a +end +``` + +### Persisting + +Your adapter handles writes for you, so most Resources define nothing here. Reach for [lifecycle hooks](/concepts/persisting#persistence-lifecycle-hooks) when you need to intervene: + +```ruby +before_attributes do |attributes| + # before attributes are assigned to the model +end + +before_save do |model| + # assigned, but not yet persisted +end + +before_commit do |model| + # saved and validated, still inside the transaction +end +``` + +The model you inspect is the model that saves. Attributes are assigned up front, so you can hold the model, check it, and change it before anything is written. The instance you were handed is the one that gets persisted: + +```ruby +employee = EmployeeResource.build(payload) + +employee.data # the model, attributes already assigned, nothing written yet +employee.data.valid? # inspect it, or modify it +employee.save # persists that same instance +``` + +Updates work the same way, reading the persisted record until you apply the payload: + +```ruby +proxy = EmployeeResource.find(payload) +proxy.data.first_name # => "asdf", straight from the database +proxy.assign_attributes(payload) +proxy.data.first_name # => "Jane", assigned but still unsaved +proxy.save(action: :update) +``` + +## Upgrading from 1.x + +The [2.0 upgrade guide](/upgrading) covers the whole migration: the three gems that folded into core, the deprecated spellings that still work but warn, and the two real behavior changes. Controllers now opt in via `Graphiti::Rails::Controller`, and `around_persistence` receives the model rather than an attributes hash. diff --git a/docs/js/authentication.md b/docs/js/authentication.md new file mode 100644 index 00000000..1477e917 --- /dev/null +++ b/docs/js/authentication.md @@ -0,0 +1,63 @@ +--- +title: 'Authentication' +sidebar_position: 7 +--- + +### Authentication + +Spraypaint supports [JSON Web Tokens](https://jwt.io/introduction). These can +be set manually, or automatically fetched from `localStorage`. + +To set manually: + +```typescript +ApplicationRecord.jwt = 'myt0k3n' +``` +> All requests will now send the header:
+> `Authorization: Token token="myt0k3n"`. + +To set via `localStorage`, store the token with a key of `jwt` and it will be set automatically. To customize the `localStorage` key: + +```typescript +ApplicationRecord.jwtStorage = "authtoken" +``` + +...or to opt-out of `localStorage` altogether: + +```typescript +ApplicationRecord.jwtStorage = false +``` + +You can control the format of the header that is sent to the +server: + +```typescript + class ApplicationRecord extends SpraypaintBase { + // ... code ... + static generateAuthHeader(token) { + return `Bearer ${token}` + } + } +``` + +```javascript + var ApplicationRecord = SpraypaintBase.extend({ + // ... code ... + static: { + generateAuthHeader: function(token) { + return "Bearer " + token; + } + } + }); +``` + +Finally, if your server returns a refreshed JWT within the `X-JWT` header, it will be used in all subsequent requests (and `localStorage` +will be updated automatically if you're using it). + +

+ + NEXT: + State Syncing + » + +

diff --git a/docs/js/ddau.md b/docs/js/ddau.md new file mode 100644 index 00000000..bf9b2ebc --- /dev/null +++ b/docs/js/ddau.md @@ -0,0 +1,20 @@ +--- +title: 'Ddau' +sidebar_position: 9 +--- + +### Data Down, Actions Up + +It's a [popular pattern](http://www.samselikoff.com/blog/data-down-actions-up) to pass data **down** to components, avoid modifying state within the component, and instead pass **actions up** to modify state. This can make complex applications easier to track and reason about, and you'll see it in client-side frameworks like React. + +To follow this pattern, use `#dup()` when passing down to your component: + +```bash + +``` + +This will create a new instance of the model with all the same state. +Avoid modifying this instance in your component and instead pass +**actions up**. + +When opting-in to [state-syncing](/js/state-syncing) these instances will sync-up whenever one of these is instances is persisted. You won't have to worry about updating the child component when the parent instance is saved. diff --git a/docs/js/extra-params.md b/docs/js/extra-params.md new file mode 100644 index 00000000..11ebfae7 --- /dev/null +++ b/docs/js/extra-params.md @@ -0,0 +1,41 @@ +--- +title: 'Extra Params' +sidebar_position: 10 +--- + +### Extra Params + +Sometimes you need to submit params that are not standard jsonapi params. One great example would be +`https://yourdomain.com/users?debug=true` which is not a param for the `UserResource` you may have, but +might enable functionality in your controller as needed. + +Invoking it is pretty straightforward, just invoke `extraParams` and pass in params and values you wish +to add to your API call when executed. + + +```typescript +YourRecord.extraParams({ debug: true }) +``` + +One common way to use this globally is to put this into a base class so it can be chained as part of +every resource. + +```typescript + @Model + export class ApplicationRecord extends SpraypaintBase { + static withDebug(): Scope { + return this.extraParams({ debug: true }) as Scope; + } + } + // unfortunately you will need to pass in the + // implementing class' type as a generic + UserRecord.withDebug().all() +``` +```javascript + const ApplicationRecord = SpraypaintBase.extend({ + static: { + withDebug: () => this.extraParams({ debug: true }); + } + }) + UserRecord.withDebug().all() +``` diff --git a/docs/js/index.md b/docs/js/index.md new file mode 100644 index 00000000..3cfc2013 --- /dev/null +++ b/docs/js/index.md @@ -0,0 +1,112 @@ +--- +title: 'Index' +sidebar_position: 1 +--- + +

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ +### Why Spraypaint? + +Contracts like JSONAPI and GraphQL treat the API like a database. When querying a database, we have two options: + + * Type the low-level query language directly (in the database world, this would be hand-typing SQL). + * Use an ORM (like Rails's `ActiveRecord`, Phoenix's `Ecto`, Django's `DjangoORM`, or Node's `Sequelize`). + +While both options have pros and cons, we tend to think ORMs have two overwhelming benefits: ***ease of use*** and ***composable queries***. We'll explore both these concepts in other sections. + +So, we want a javascript ORM for our JSONAPI "database". Because `ActiveRecord` is arguably the most well-known ORM, we've tried to match its interface to make this library accessible to new users. That said, you'll find we've tried to favor *explicitness* over *implicitness* in order to avoid common `ActiveRecord` pitfalls. + + Typescript + Javascript +```typescript +// Spraypaint is like "ActiveRecord in Javascript". It can: +// +// * Deeply nest reads and writes +// * Automatically handle validation errors +// * Replace *ux patterns +// * ...and much more! + +// define models +@Model() +class ApplicationRecord extends SpraypaintBase { + static baseUrl = "http://my-api.com" + static apiNamespace = "/api/v1" +} + +@Model() +class Person extends ApplicationRecord { + static jsonapiType = "people" + + @Attr() firstName: string + @Attr() lastName: string + + get fullName() { + return `${this.firstName} ${this.lastName}` + } +} + +// execute queries +Person + .where({ first_name: 'John' }) + .order({ created_at: 'desc' }) + .per(10).page(2) + .includes({ jobs: 'company' }) + .select({ people: ['first_name', 'last_name'] }) + +// persist data +let person = new Person({ firstName: 'Jane' }) +person.save() +``` + +```javascript +// Spraypaint is like "ActiveRecord in Javascript". It can: +// +// * Deeply nest reads and writes +// * Automatically handle validation errors +// * Replace *ux patterns +// * ...and much more! + +var spnt = require('spraypaint/dist/spraypaint') + +// define models +const ApplicationRecord = spnt.JSORMBase.extend({ + static: { + baseUrl: 'http://my-api.com', + apiNamespace: '/api/v1' + } +}) + +const Person = ApplicationRecord.extend({ + attrs: { + firstName: spnt.attr(), + lastName: spnt.attr() + }, + methods: { + fullName: function() { + return this.firstName + ' ' + this.lastName; + } + } +}) + +// execute queries +Person + .where({ first_name: 'John' }) + .order({ created_at: 'desc' }) + .per(10).page(2) + .includes({ jobs: 'company' }) + .select({ people: ['first_name', 'last_name'] }) + +// persist data +var person = new Person({ firstName: 'Jane' }) +person.save() +``` + +### Where to Go Next + + * [Installation](/js/installation) - install spraypaint and connect it to your API + * [Models](/js/models) - define models, attributes, and relationships + * [Reads](/js/reads) - query your API with a composable, ActiveRecord-like interface + * [Writes](/js/writes) - create, update, and destroy records diff --git a/docs/js/installation.md b/docs/js/installation.md new file mode 100644 index 00000000..f1862448 --- /dev/null +++ b/docs/js/installation.md @@ -0,0 +1,120 @@ +--- +title: 'Installation' +sidebar_position: 2 +--- + +### Installation + +Installation is straightforward. Since we use `fetch` underneath the hood, we recommend installing alongside a `fetch` polyfill. + +If using `yarn`: + +```bash +$ yarn add spraypaint isomorphic-fetch +``` + +If using `npm`: + +```bash +$ npm install spraypaint isomorphic-fetch +``` + +Now import it: + +```typescript +import { + Model, + SpraypaintBase, + Attr, + BelongsTo, + HasMany + // etc +} from "spraypaint" +``` + +```javascript +const { + SpraypaintBase, + attr, + belongsTo, + hasMany + // etc +} = require("spraypaint/dist/spraypaint") +``` + +...or, if you're avoiding JS modules, `spraypaint` will be available as a global in the browser. + +### Typescript + +Spraypaint works with modern TypeScript. Depending on your `tsconfig.json` settings, you may need a `!` after each attribute and relationship declaration: + +```typescript +@Attr first_name!: string +@HasMany() positions!: Position[] +``` + +This is because of [Strict Class Initialization](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#strict-class-initialization) - `strictPropertyInitialization` expects every declared class field to be assigned in the constructor, which Spraypaint's decorators handle at runtime rather than at construction time. For the purposes of Spraypaint, we don't need this check. Remove the need for `!` (as the rest of these guides do) by setting + +`"strictPropertyInitialization": false` + +in `tsconfig.json`. + +### Connecting to the API + +Just like `ActiveRecord`, our models will inherit from a base class that holds connection information (`ApplicationRecord`, or `ActiveRecord::Base` in Rails < 5): + +```typescript +@Model() +class ApplicationRecord extends SpraypaintBase { + static baseUrl = "http://my-api.com" + static apiNamespace = "/api/v1" +} +``` + +```javascript +const ApplicationRecord = SpraypaintBase.extend({ + static: { + baseUrl: "http://my-api.com", + apiNamespace: "/api/v1" + } +}) +``` + +All URLs follow the following pattern: + + * `baseUrl` + `apiNamespace` + `jsonapiType` + +As you can see above, typically `baseUrl` and `apiNamespace` are set on a top-level `ApplicationRecord` (though any subclass can override). `jsonapiType`, however, is set per-model - see [Models](/js/models) for how to define it. + +> **TIP**: Avoid CORS and use relative paths by setting `baseUrl` to `""` + +> **TIP**: You can always use the `endpoint` option to override this pattern and set the endpoint manually. + +#### Setting Application Name + +It can be helpful to send the name of your client application in request headers. With this information, servers can keep track of which clients are hitting which APIs. + +To do this: + +```typescript +@Model() +class Person extends ApplicationRecord { + static clientApplication = "sales-backend" +} +``` + +```javascript +const Person = ApplicationRecord.extend({ + static: { + clientApplication: "sales-backend" + } +}) +``` + +

+ + NEXT: + Models + » + +

diff --git a/docs/js/middleware.md b/docs/js/middleware.md new file mode 100644 index 00000000..199cfaba --- /dev/null +++ b/docs/js/middleware.md @@ -0,0 +1,72 @@ +--- +title: 'Middleware' +sidebar_position: 6 +--- + +### Middleware + +Middleware is helpful whenever you want to globally intercept request. +This is accomplished by assigning a `MiddlewareStack` to your `ApplicationRecord`. Each stack has `beforeFilters` and `afterFilters` where you can globally modify requests. If you `throw("abort")`, the +promise will be rejected. + +Example: redirecting to the login page every time the server returns `401`: + +```typescript + import { MiddlewareStack } from 'spraypaint' + + let middleware = new MiddlewareStack() + middleware.afterFilters.push((response, json) => { + if (response.status === 401) { + window.location.href = "/login" + throw("abort") + } + }) + + ApplicationRecord.middlewareStack = middleware +``` + +```javascript + var MiddlewareStack = spraypaint.MiddlewareStack; + + var middleware = new MiddlewareStack(); + middleware.afterFilters.push(function(response, json) { + if (response.status === 401) { + window.location.href = "/login"; + throw("abort"); + } + }); + + ApplicationRecord.middlewareStack = middleware; +``` + +Example: adding a custom header before the request is sent: + +```typescript + import { MiddlewareStack } from 'spraypaint' + + let middleware = new MiddlewareStack() + middleware.beforeFilters.push((url, options) => { + options.headers["CUSTOM-HEADER"] = "whatever" + }) + + ApplicationRecord.middlewareStack = middleware +``` + +```javascript + var MiddlewareStack = spraypaint.MiddlewareStack; + + var middleware = new MiddlewareStack(); + middleware.beforeFilters.push(function(url, options) { + options.headers["CUSTOM-HEADER"] = "whatever"; + }); + + ApplicationRecord.middlewareStack = middleware; +``` + +

+ + NEXT: + Authentication + » + +

diff --git a/docs/js/models.md b/docs/js/models.md new file mode 100644 index 00000000..af34e48c --- /dev/null +++ b/docs/js/models.md @@ -0,0 +1,202 @@ +--- +title: 'Models' +sidebar_position: 3 +--- + +### Defining Models + +Once your `ApplicationRecord` base class is [connected to the API](/js/installation#connecting-to-the-api), define a model by giving it a `jsonapiType`: + +```typescript +@Model() +class Person extends ApplicationRecord { + static jsonapiType = "people" +} +``` + +```javascript +const Person = ApplicationRecord.extend({ + static: { + jsonapiType: "people" + } +}) +``` + +With the above configuration, all `Person` endpoints will begin `http://my-api.com/api/v1/people`. + +### Defining Attributes + +`ActiveRecord` automatically sets attributes by introspecting database columns. We could do the same - `swagger.json` is our schema - but tend to agree with those who feel this aspect of `ActiveRecord` is a bit too "magical". In addition, explicitly defining our attributes can be used to track which applications are using which attributes of the API. + +Though this is configurable, by default we expect the API to be `under_scored` and attributes to be `camelCased`. + +```typescript +@Model() +class Person extends ApplicationRecord { + // ... code ... + @Attr() firstName: string + @Attr() lastName: string + @Attr() age: number + + get fullName() : string { + return `${this.firstName} ${this.lastName}` + } +} + +let person = new Person({ firstName: "John" }) +person.firstName // "John" +person.lastName = "Doe" +person.attributes // { firstName: "John", lastName: "Doe" } +person.fullName // "John Doe" +``` + +```javascript +const attr = spraypaint.attr +const Person = ApplicationRecord.extend({ + // ... code ... + attrs: { + firstName: attr(), + lastName: attr(), + age: attr() + }, + methods: { + fullName: function() { + return this.firstName + " " + this.lastName; + } + } +}) + +var person = new Person({ firstName: "John" }) +person.firstName // "John" +person.lastName = "Doe" +person.attributes // { firstName: "John", lastName: "Doe" } +person.fullName() // "John Doe" +``` + +Attributes can be marked read-only, so they are never sent to the server on a write request: + +```typescript +@Attr({ persist: false }) createdAt: string +@Attr({ persist: false }) updatedAt: string +``` + +```javascript +attrs: { + createdAt: attr({ persist: false }), + updatedAt: attr({ persist: false }) +} +``` + +### Defining Relationships + +Just like `ActiveRecord`, there are `HasMany`, `BelongsTo`, and `HasOne` relationships: + +```typescript +@Model() +class Dog extends ApplicationRecord { + // ... code ... + @BelongsTo() person: Person[] +} + +class Person extends ApplicationRecord { + // ... code ... + @HasMany() dogs: Dog[] +} +``` + +```javascript +const hasMany = spraypaint.hasMany +const belongsTo = spraypaint.belongsTo + +const Person = ApplicationRecord.extend({ + // ... code ... + attrs: { + dogs: hasMany() + } +}) + +const Dog = ApplicationRecord.extend({ + // ... code ... + attrs: { + person: belongsTo() + } +}) +``` + +By default, we expect the relationship name to correspond to a pluralized `jsonapiType` on a separate `Model`. If your models don't use this convention, feel free to supply it explicitly: + +```typescript +@Model() +class Dog extends ApplicationRecord { + // ... code ... + @BelongsTo('people') owner: Person[] +} + +// alternatively, specify the class directly + +class Dog extends ApplicationRecord { + // ... code ... + @BelongsTo(Person) owner: Person[] +} +``` + +```javascript +const Dog = ApplicationRecord.extend({ + // ... code ... + attrs: { + owner: belongsTo('people') + } +}) +``` + +Relationships can be: + +* Assigned via constructor +* Assigned directly +* Automatically loaded via `.includes()` (see [reads](/js/reads)) +* Saved in a single request `.save({ with: 'dogs' })` (see +[writes](/js/writes)) + +```typescript +let dog = new Dog({ name: "Fido" }) +let person = new Person({ dogs: [dog] }) +person.dogs[0].name // "Fido" + +let person = new Person() +person.dogs = [dog] +person.dogs[0].name // "Fido" + +// Will auto-create Dog instance +let person = new Person({ dogs: [{ name: "Scooby" }] }) +person.dogs[0].name // "Scooby" + +let person = (await Person.includes('dogs')).data +person.dogs // array of Dog instances from the server +``` + +```javascript +var dog = new Dog({ name: "Fido" }) +var person = new Person({ dogs: [dog] }) +person.dogs[0].name // "Fido" + +let person = new Person() +person.dogs = [dog] +person.dogs[0].name // "Fido" + +// Will auto-create Dog instance +var person = new Person({ dogs: [{ name: "Scooby" }] }) +person.dogs[0].name // "Scooby" + +Person.includes('dogs').then((response) => { + var person = response.data + person.dogs // array of Dog instances from the server +}) +``` + +

+ + NEXT: + Reads + » + +

diff --git a/docs/js/reads.md b/docs/js/reads.md new file mode 100644 index 00000000..6705d74d --- /dev/null +++ b/docs/js/reads.md @@ -0,0 +1,494 @@ +--- +title: 'Reads' +sidebar_position: 4 +--- + +The interface for read operations is a simpler version of the [ActiveRecord Query Interface](http://guides.rubyonrails.org/active_record_querying.html). Instead of generating SQL, we'll be generating JSONAPI requests. + +## Basic Finders + +Execute queries with `.all()`, `find()`, or `.first()`: + +```typescript +let response = await Post.all() +response.data // array of Post instances +``` +```javascript +Post.all().then(function(response) { + response.data // array of Post instances +}); +``` +
+

GET /posts

+
+ +```typescript +let response = await Post.find(123) +response.data // Post instance +``` +```javascript +Post.find(123).then(function(response) { + response.data // Post instance +}); +``` +
+

GET /posts/123

+
+ +```typescript +let response = await Post.first() +response.data // Post instance +``` +```javascript +Post.first().then(function(response) { + response.data // Post instance +}); +``` +
+

GET /posts?page[size]=1

+
+ +## Composable Queries with Scopes + +The beauty of ORMs is their ability to compose queries. We'll be doing this by chaining together `Scope`s (query fragments). All of the methods you see on this page can be chained together - the request will not fire until the chain ends with `all()`, `first()`, or `find`. Example: + +```typescript +let scope = Post.order({ name: "desc" }) + +if (someCheckboxIsChecked) { + scope = scope.where({ important: true }) +} else { + scope = scope.where({ important: false }) +} + +scope.all() // request fires +``` + +```javascript +var scope = Post.order({ name: "desc" }); + +if (someCheckboxIsChecked) { + scope = scope.where({ important: true }); +} else { + scope = scope.where({ important: false }); +} + +scope.all() // request fires +``` +
+

/posts?sort=-name&filter[important]=true

+

/posts?sort=-name&filter[important]=false

+
+ +In practice, you'll probably have some scopes you want to re-use across different contexts. A best practice is to store these scopes as class methods (static methods) in the model: + +```typescript +class Post extends ApplicationRecord { + // ... code ... + static superImportant() { + return this + .where({ ranking_gt: 8 }) + .order({ ranking: 'desc' }) + .stats({ total 'count' }) + } +} + +// get 10 super important posts +let scope = Post.superImportant().per(10) +scope.all() // fire query +``` + +```javascript +const Post = ApplicationRecord.extend({ + // ... code ... + static: { + superImportant() { + return this + .where({ ranking_gt: 8 }) + .order({ ranking: 'desc' }) + .stats({ total 'count' }) + } + } +}) + +// get 10 super important posts +var scope = Post.superImportant().per(10); +scope.all() // fire query +``` +
+

/posts?sort=-ranking&stats[total]=count&page[size]=10&filter[ranking_gt]=8

+
+ +## Metadata + +The [meta information](http://jsonapi.org/format/#document-meta) of the JSONAPI response is available as a POJO on the response: + +```typescript +let response = await Post.all() +response.meta // { stats: { total: { count: 100 } } } +``` +```javascript +await Post.all().then(function(response) { + response.meta // { stats: { total: { count: 100 } } } +}) +``` + +## Promises and Async/Await + +The result of `all()`, `first()` or `find` is a [Promise](https://developers.google.com/web/fundamentals/primers/promises). The promise will resolve to a `Response` object. + +A `Response` object has three keys - `data`, `meta`, and `raw`. `data` - the one you'll be using the most - will be a `Model` instance (or array of `Model`) instances. `meta` will be the [Meta Information](http://jsonapi.org/format/#document-meta) returned by the API (mostly used for statistics in our case). `raw` is only used to introspect the raw response document. + +```typescript +Post.all().then((response) => { + response.data // array of Post instances + response.meta // js object from the server + response.raw // js response document +}) +``` + +```javascript +Post.all().then(function(response) { + response.data // array of Post instances + response.meta // js object from the server + response.raw // js response document +}); +``` +
+

/posts

+
+ +Hopefully you're running in an environment that supports ES7's [Async/Await](https://hackernoon.com/6-reasons-why-javascripts-async-await-blows-promises-away-tutorial-c7ec10518dd9). This makes things even easier: + +```typescript +let { data } = await Post.all() +data // array of Post instances + +// alternatively + +let posts = (await Post.all()).data +posts // array of Post instances +``` +
+

/posts

+
+ +## Filtering + +Use `#where()` to apply filters: + +```typescript +Post.where({ important: true }).all() +``` +
+

/posts?filter[important]=true

+
+ +`#where()` clauses can be chained together. If the same key is seen twice, it will be overridden: + +```typescript +Post + .where({ important: true }) + .where({ ranking: 10 }) + .where({ important: false }) + .all() +``` +
+

/posts?filter[important]=false&filter[ranking]=10

+
+ +`#where()` clauses are based on **server implementation**. The key should be exactly as the server understands it. Here are some common conventions we promote: + +```typescript +// id greater than 5 +Post.where({ id_gt: 5 }).all() + +// id greater than or equal to 5 +Post.where({ id_gte: 5 }).all() + +// id less than 5 +Post.where({ id_lt: 5 }).all() + +// id less or equal to 5 +Post.where({ id_lte: 5 }).all() + +// title starts with "foo" +Post.where({ title: { prefix: "foo" } }).all() + +// OR these two values +Post.where({ status_or: ['draft', 'review'] }) + +// AND these two values (default) +Post.where({ status: ['draft', 'review'] }) +``` + +### Escaping Values + +[Graphiti treats a comma as a delimiter of multiple values](/concepts/resources#escaping-values). To escape the comma and tell Graphiti this is a single value, wrap it in `{{curlies}}`: + +```typescript +Post.where({ title: "{{Hello World, here I am}}" }) +``` + +## Sorting + +Use `#order()` to sort. + +If passed a string, it will default to **ascending**: + +```typescript +Post.order("title").all() +``` +
+

/posts?sort=title

+
+ + +Otherwise, pass an object: + +```typescript +Post.order({ title: "desc" }).all() +``` +
+

/posts?sort=-title

+
+ +For multisort, chain multiple `#order()` clauses: + +```typescript +Post + .order({ title: "desc" }) + .order("ranking") + .all() +``` +
+

/posts?sort=-title,ranking

+
+ +## Pagination + +Use `#per()` to set the limit per page: + +```typescript +Post.per(10).all() +``` +
+

/posts?page[size]=10

+
+ +Use `#page()` to set the current page: + +```typescript +Post.page(5).all() +``` +
+

/posts?page[number]=5

+
+ +When chained together (10 per page, the 5th page): + +```typescript +Post.page(5).per(10).all() +``` +
+

/posts?page[size]=10&page[number]=5

+
+ +## Fieldsets + +### Sparse Fieldsets + +Use `#select()` to limit the fields returned by the server: + +```typescript +Post.select(['title', 'status']).all() +``` +
+

/posts?fields[posts]=title,status

+
+ +When dealing with relationships, it may be easier to pass an object, where the key is the corresponding JSONAPI type. This will be exactly what's sent to the server in `?fields`: + +```typescript +Post.select({ + posts: ['title', 'status'], + comments: ['created_at'] +}).all() +``` +
+

/posts?fields[posts]=title,status&fields[comments]=created_at

+
+ +### Extra Fieldsets + +Use `#selectExtra()` to explicitly request a field that doesn't usually come back (often computationally expensive): + +```typescript +Post.selectExtra(['highlights', 'cumulative_ranking']).all() +``` +
+

/posts?extra_fields[posts]=highlights,cumulative_ranking

+
+ +Just like the `select` example above, feel free to pass an object specifying the fields for each relationship. + +## Includes + +Use `#includes()` to ["sideload"](http://jsonapi.org/format/#fetching-includes) associations: + +```typescript +Post.includes("comments").all() +``` +
+

/posts?include=comments

+
+ +You can also pass an array of associations: + +```typescript +Post.includes(["blog", "comments"]).all() +``` +
+

/posts?include=blog,comments

+
+ +Or an object for nested associations: + +```typescript +Post.includes(["blog", { comments: "author" }]).all() +``` +
+

/posts?include=blog,comments.author

+
+ +## Nested Queries + +We can nest all read operations at any level of the graph. Let's say we wanted to fetch all `Post`s and their `Comment`s...but only return comments that are `active`, sorted by `created_at` descending. We can create a `Comment` scope as normal, then `#merge()` it into our `Post` scope: + +```typescript +let commentScope = Comment + .where({ active: true }) + .order({ created_at: "desc" }) +Post + .includes("comments") + .merge({ comments: commentScope }) + .all() +``` + +```javascript +var commentScope = Comment + .where({ active: true }) + .order({ created_at: "desc" }) +Post + .includes("comments") + .merge({ comments: commentScope }) + .all() +``` +
+

/posts?include=comments&filter[comments][active]=true&sort=-comments.active

+
+ +Because this can get verbose, it's often desirable to store it on the class: + +```typescript +class Comment extends ApplicationRecord { + // ... code ... + static recent() { + return this + .where({ active: true }) + .order({ created_at: "desc" }) + } +} + +Post.merge({ comments: Comment.recent() }).all() +``` + +```javascript +const Comment = ApplicationRecord.extend({ + // ... code ... + static: { + recent: function() { + return this + .where({ active: true }) + .order({ created_at: "desc" }) + } + } +}) + +Post + .includes("comments") + .merge({ comments: Comment.recent() }) + .all() +``` + +Any number of scopes can be merged in. Just remember to `#include()` and `#merge()` relationship names **as the server understands them**: + +```typescript +class Dog extends ApplicationRecord { + @BelongsTo() person: Person +} + +// We've modeled this as Dog > person in javascript +// And Person is jsonapiType "people" +// But the server defined the relationship as "owner" +Dog.includes("owner").merge({ owner: Person.limitedFields() }) +``` + +```javascript +const Dog = ApplicationRecord.extend({ + // ... code ... + methods: { + person: belongsTo() + } +}) + +// We've modeled this as Dog > person in javascript +// And Person is jsonapiType "people" +// But the server defined the relationship as "owner" +Dog.includes("owner").merge({ owner: Person.limitedFields() }) +``` + +## Statistics + +Use `#stats()` to request statistics. Access stats within `meta`: + +```typescript +let { data } = await Post.stats({ total: "count" }).all() +data.meta.stats.total.count // the total count +``` + +```javascript +Post.stats({ total: "count" }).all().then(function(response) { + response.meta.stats.total.count // the total count +}) +``` +
+

/posts?stats[total]=count

+
+ +Stats are always independent of pagination. If you request the total count, you'll get the total count even if you're limiting to 10 per page. This means to get **only** statistics - avoid returning `Post` instances altogether - request `0` results per page: + +```typescript +let { data } = await Post.per(0)stats({ total: "count" }).all() +data.meta.stats.total.count // the total count +``` + +```javascript +Post + .per(0) + .stats({ total: "count" }) + .all().then(function(response) { + response.meta.stats.total.count // the total count + }) +``` +
+

/posts?stats[total]=count&page[size]=0

+
+ +

+ + NEXT: + Writes + » + +

diff --git a/docs/js/state-syncing.md b/docs/js/state-syncing.md new file mode 100644 index 00000000..4316736c --- /dev/null +++ b/docs/js/state-syncing.md @@ -0,0 +1,100 @@ +--- +title: 'State Syncing' +sidebar_position: 8 +--- + +### State Syncing + +You may have encountered state management libraries like +[Redux](https://redux.js.org) or [Vuex](https://vuex.vuejs.org/en/intro.html). These are fantastic libraries, but their usefulness is lessened with Spraypaint. As a full-fledged model layer, Spraypaint manages state for you, automatically. + +If you opt-in to this feature: + +```typescript +ApplicationRecord.sync = true +``` + +Instances will sync up whenever the server tells us about updated state. +Consider the scenario where an instance is initially loaded, then separately polled in the background: + +```typescript + let person = (await Person.find(1)).data + + let poll = () => { + await Person.find(1) + setTimeout(poll, 1000) + } + poll() +``` + +```javascript + Person.find(1).then(function(response) { + var person = response.data; + }); + + var poll = function() { + Person.find(1); + setTimeout(poll, 1000); + } + poll() +``` + +Our `poll()` function **never assigns or updates `person`**. But if the server returns an updated `name` attribute, **`person.name` will be automatically updated**. This is true even if `person.name` +is bound in 17 different nested components. + +Instances can still update their attributes independently - we only sync +when the server returns updated data: + +```typescript + let instanceA = (await Person.find(1)).data + let instanceB = (await Person.find(1)).data + + instanceA.name // "Jane" + instanceB.name // "Jane" + + instanceB.name = "Silvia" + instanceA.name // "Jane" + instanceB.name // "Silvia" + + await instanceB.save() + instanceA.name // "Silvia" + instanceB.name // "Silvia" +``` + +```javascript + var instanceA, instanceB; + Person.find(1).then(function(response) { + instanceA = response.data; + }); + Person.find(1).then(function(response) { + instanceB = response.data; + }); + + instanceA.name // "Jane" + instanceB.name // "Jane" + + instanceB.name = "Silvia" + instanceA.name // "Jane" + instanceB.name // "Silvia" + + instanceB.save().then(function() { + instanceA.name // "Silvia" + instanceB.name // "Silvia" + }); +``` + +#### Gotchas + +Under the hood, instances are listening for updates from a central data +store. This means that you'll want to remove listeners whenever you no +longer need the instance - otherwise it will never be garbage collected +properly. To remove a listener: + +```typescript +instance.unlisten() +``` + +In practice, when developing in a SPA, you'll want to `#unlisten()` +whenever a view is destroyed and model instances no longer need to be referenced. If +you are using VueJS, this is done automatically by adding [spraypaint-vue](https://github.com/graphiti-api/spraypaint-vue) +to your application. diff --git a/docs/js/writes.md b/docs/js/writes.md new file mode 100644 index 00000000..6c4fd585 --- /dev/null +++ b/docs/js/writes.md @@ -0,0 +1,373 @@ +--- +title: 'Writes' +sidebar_position: 5 +--- + +Similar to `ActiveRecord`, you can call `#save()` on a model instance. Spraypaint will [create](http://jsonapi.org/format/#crud-creating) (`POST`) or [update](http://jsonapi.org/format/#crud-updating) (`PATCH`) as needed. + +`#save()` returns a `Promise` that will resolve a `boolean` - `true` when the server returns a 200-ish response code, `false` when the server returns a `422` response code (see [validations](/js/writes#validations)). As always, anything else will reject the promise. + +```typescript + let blog = new Blog({ title: "My Blog" }) + let success = await blog.save() // POST /blogs + console.log(success) // true/false + + blog.title = "Updated Title" + success = await blog.save() // PUT /blogs/:id + console.log(success) // true/false +``` + +```javascript + var blog = new Blog({ title: "My Blog" }); + // POST /blogs + blog.save().then(function(success) { + console.log(success); // true/false + + blog.title = "Updated Title": + // PUT /blogs/:id + blog.save().then(function(success) { + console.log(success) // true/false + }); + }); +``` + +After saving, the instance will automatically pick up any server-assigned attributes: + +```typescript + let post = new Post() + await post.save() + post.id // server-assigned value + post.createdAt // server-assigned value +``` + +```javascript + var post = new Post(); + post.save().then(function(success) { + post.id // server-assigned value + post.createdAt // server-assigned value + }); +``` + +If a `Model` was instantiated with data from the server, `isPersisted` will return `true`. This means that we can assign IDs on the client without any adverse behavior. We can also manually mark objects as persisted for testing purposes: + +```typescript + let blog = new Blog({ id: 123 }) + blog.isPersisted // false + await blog.save() // POST /blogs + blog.isPersisted // true + blog.id // 123 + + // Manually mark an instance as persisted + blog = new Blog({ id: 123 }) + blog.isPersisted = true + await blog.save() // PUT /blogs/123 +``` + +```javascript + var blog = new Blog({ id: 123 }); + blog.isPersisted // false + // POST /blogs + blog.save().then(function(response) { + blog.isPersisted // true + blog.id // 123 + }); + + // Manually mark an instance as persisted + var blog = new Blog({ id: 123 }); + blog.isPersisted = true + blog.save() // PUT /blogs/123 +``` + +Notably, **only dirty (changed) attributes will be sent to the server**. This prevents race conditions and unexpected side-effects. In the following example, `Post` has attributes `title`, `description`, and `createdAt`: + +```typescript + let post = (await Post.first()) + post.title = "updated" + // ONLY title sent to the server + await post.save() + // Title is now synced with the server + post.description = "updated" + // ONLY description sent to the server + await post.save() +``` + +```javascript + Post.first().then(function(response) { + var post = response.data; + post.title = "updated"; + // ONLY title sent to the server + post.save().then(function(response) { + // Title is now synced with the server + post.description = "updated"; + // ONLY description sent to the server + post.save(); + }); + }); +``` + +## Validations + +JSONAPI Suite is already set up to return validation errors with a `422` response code and JSONAPI-compliant [errors payload](http://jsonapi.org/format/#errors). Those errors will be automatically assigned, and removed on subsequent requests: + +```typescript + let success = await post.save() + console.log(success) // false + post.errors.title // { message: "Can't be blank", ... } + post.title = "no longer blank" + success = await post.save() + console.log(success) // true + post.errors // {} +``` + +```javascript + post.save().then(function(success) { + console.log(success) // false + post.errors.title // { message: "Can't be blank", ... } + post.title = "no longer blank" + post.save().then(function(success) { + console.log(success); // true + post.errors // {} + }); + }) +``` + +## Dirty Tracking + +When an attribute has been modified, but has not yet been saved to the server, it is considered "dirty". Use `#isDirty()` to see if any attribute is dirty, use the `#changes()` method to see all dirty attributes. + +```typescript + let post = await Post.first() + post.title // "original" + post.isDirty() // false + post.changes() // {} + + post.title = "changed" + post.isDirty() // true + post.changes() // { title: ["original", "changed"] } + + await post.save() + post.isDirty() // false + post.changes() // {} +``` + +```javascript + Post.first().then(function(response) { + var post = response.data; + + post.title; // "original" + post.isDirty(); // false + post.changes(); // {} + + post.title = "changed"; + post.isDirty(); // true + post.changes(); // { title: ["original", "changed"] } + + post.save().then(function(success) { // true + post.isDirty(); // false + post.changes(); // {} + }); + }); +``` + +> Remember, only dirty attributes are sent to the server when `#save()` +> is called. + +`#isDirty()` *can* take into account relationships - just pass a string, array, or object or relationship names. A relationship is considered dirty if: + +* Any objects in the relationship have dirty attributes +* An object was removed from a `hasMany` relationship +* An object was added to a `hasMany` relationship +* Any object within the relationship was replaced with a different +object. + +```typescript + let post = await Post.first() + post.comments[0].text = "my comment" + post.isDirty("comments") // true + + post = await Post.first() + post.comments.push(new Comment()) + post.isDirty("comments") // true + + post = await Post.first() + post.comments.splice(1, 1) + post.isDirty("comments") // true + + post = await Post.first() + post.blog // an existing Blog instance + post.blog = (await Blog.first()).data + post.isDirty("blog") // true + + // check nested relationships + post.isDirty(["blog", { comments: "author" }]) +``` + +```javascript + Post.first().then(function(response) { + var post = response.data; + post.comments[0].text = "my comment"; + post.isDirty("comments"); // true + }); + + Post.first().then(function(response) { + var post = response.data; + post.comments.push(new Comment()); + post.isDirty("comments"); // true + }); + + Post.first().then(function(response) { + var post = response.data; + post.comments.splice(1, 1); + post.isDirty("comments"); // true + }); + + Post.first().then(function(response) { + var post = response.data; + post.blog; // an existing Blog instance + + Blog.first().then(function(blog) { + post.blog = (await Blog.first()).data + post.isDirty("blog") // true + }); + }); + + // check nested relationships + post.isDirty(["blog", { comments: "author" }]) +``` + +If you need to reset dirty tracking, call `#reset()` + +```typescript + let post = await Post.first() + post.title // "original" + post.title = "changed" + post.isDirty() // true + post.reset() + post.title // "changed" + post.isDirty() // false +``` + +```javascript + Post.first().then(function(post) { + post.title; // "original" + post.title = "changed"; + post.isDirty() // true + post.reset(); + post.title; // "original" + post.isDirty() // false + }); +``` + +## Nested Writes + +You can write a `Model` and all of its relationships in a single request. Keep in mind normal dirty tracking rules still apply - nothing is sent to the server unless it is dirty. + +```typescript + let author = new Author() + let comment = new Comment({ author }) + let post = new Post({ comments: [comment] }) + + // post.save({ with: "comments" }) + // post.save({ with: ["comments", "blog"] }) + post.save({ with: { comments: 'author' }}) +``` + +```javascript + var author = new Author(); + var comment = new Comment({ author: author }); + var post = new Post({ comments: [comment] }); + + // post.save({ with: "comments" }) + // post.save({ with: ["comments", "blog"] }) + post.save({ with: { comments: "author" }}); +``` + +Use `model.isMarkedForDestruction = true` to delete the associated object. Use `model.isMarkedForDisassociation = true` to remove the association without deleting the underlying object: + +```typescript + let post = (await Post.includes("comments").first()).data + post.comments[0].isMarkedForDestruction = true + post.comments[1].isMarkedForDisassociation = true + + // destroys the first comment + // disassociates the second comment + await post.save({ with: "comments" }) +``` + +```javascript + Post.includes("comments").first().then(function(response) { + var post = response.data; + post.comments[0].isMarkedForDestruction = true; + post.comments[1].isMarkedForDisassociation = true; + + // destroys the first comment + // disassociates the second comment + post.save({ with: "comments" }) + }); +``` + +You may want to send *only* the `id` of the related object to the server - ensuring the models are associated without updating attributes by accident. Just add `.id` to the relationship name: + +```typescript + post.save({ with: "comments.id" }) +``` + +```javascript + post.save({ with: "comments.id" }) +``` + +## Deferred Action + +If your update or destroy action takes a long time then the server can respond with status code `202 Accepted` and include background job object in the payload. + +Example response: +```http +HTTP/1.1 202 Accepted +Content-Type: application/vnd.api+json + +{ + "data": { + "type": "background_jobs", + "id": "550e8400-e29b-41d4-a716-446655440000", + "attributes": { + "status": "pending" + } + } +} +``` + +You will need to give the model object a callback called `onDeferredDestroy` or `onDeferredUpdate`. Spraypaint will then call your callback with the deserialized object included in the payload. + +```typescript +let person = new Person({ firstName: 'Jane' }) +person.onDeferredUpdate = (job: any) => { + handleBackgroundJob(job); +} +person.save() + +person.onDeferredDestroy = (job: any) => { + handleBackgroundJob(job); +} +person.destroy() +``` + +```javascript +const person = new Person({ firstName: 'Jane' }); +person.onDeferredUpdate = (job) => { + handleBackgroundJob(job); +}; +person.save(); + +person.onDeferredDestroy = (job) => { + handleBackgroundJob(job); +}; +person.destroy(); +``` + +

+ + NEXT: + Middleware + » + +

diff --git a/docs/reference/vandal.md b/docs/reference/vandal.md new file mode 100644 index 00000000..ffb589f3 --- /dev/null +++ b/docs/reference/vandal.md @@ -0,0 +1,63 @@ +--- +title: 'Vandal' +--- + +# Vandal +Vandal is the Graphiti UI. It's helpful for exploring data, testing and +generating URLs. To take Vandal for a spin, [view our sample app](https://jsonapi-employee-directory.herokuapp.com/vandal) (*initial load may take a second*). + +
+ + + + +## Installation {#installation} + +### Installing via Template {#installing-via-template} + +If you ran our [application template](/getting-started/installation), +you already have Vandal installed. Check your routes to see it mounted. + +### Installing via Gem {#installing-via-gem} + +* Add the `vandal_ui` gem. +* Run `rake vandal:install` +* Mount the engine: + +```ruby +# config/routes.rb +scope path: "/api/v1", defaults: {format: :jsonapi} do + # ... routes ... + mount VandalUi::Engine, at: '/vandal' +end +``` + +That's it! Vandal will dynamically generate a schema at `/vandal/schema.json`, and you can view the UI at `/vandal`. + +### Manual Installation {#manual-installation} + +[Vandal](https://github.com/graphiti-api/vandal) is a VueJS +application. Grab the [dist files](https://github.com/graphiti-api/vandal/tree/master/dist) and put them anywhere you'd like. + +`index.html` has a placeholder, `__SCHEMA_PATH__`. Replace +this with a URL hosting your schema, and you'll be good to go. + +## Usage {#usage} + +First, make sure your schema is being correctly generated. You should +see Vandal make a request something like `/vandal/schema.json` - make +sure that looks correct. If it doesn't, you may need to bounce your +server. + +After selecting an endpoint, use the left rail to configure your +request. Click a relationship once to include it in the response. +If a relationship is included, you can click any row in the table to +view related data. + +Click a relationship twice and you can configure the deep query of +the associated Resource. In other words, if you're fetching Posts and +Comments, click `comments` twice to say things like "only active +comments should be returned". + +When you hit 'submit', the top URL bar will change to reflect your query +and results will show in the center table. diff --git a/docs/reference/why.md b/docs/reference/why.md new file mode 100644 index 00000000..f8743607 --- /dev/null +++ b/docs/reference/why.md @@ -0,0 +1,13 @@ +--- +title: 'Why REST?' +--- + +# Why REST? + +Graphiti builds on REST rather than replacing it, which is worth a short explanation if you're weighing it against GraphQL. + +The complaints that motivated GraphQL are real. REST APIs often make clients do several round trips and still hand back the wrong shape of data. But those are complaints about how REST APIs are usually built, not about REST. Add eager-loading and a schema to REST and the complaints go away, and you keep the parts of REST that are hard to get back once you've left: addressable URLs, HTTP caching, and Links that let the server change how a relationship resolves without breaking clients. + +The other half is conventions. A GraphQL schema is hand-written per type, so filtering and sorting get reinvented on every team. One API spells it `name_contains`, another `name_LIKE`, another exposes no multisort at all. JSON:API already answers those questions, so `?filter[name][prefix]=Ja&sort=-created_at&page[size]=10` means the same thing on every endpoint of every Graphiti API. You define the Resource. The query interface follows from the attribute types. + +That's the whole tradeoff: fewer decisions per endpoint, at the cost of a fixed request and response format. diff --git a/docs/topics/authorization.md b/docs/topics/authorization.md new file mode 100644 index 00000000..35b10e4d --- /dev/null +++ b/docs/topics/authorization.md @@ -0,0 +1,155 @@ +--- +title: 'Authorization' +--- + +Graphiti authorization happens at three independent layers: which records a query can ever see (`base_scope`), which attributes are readable/writable on those records, and which relationships can be sideloaded or sideposted. Each layer is enforced separately, so a guard on one doesn't imply anything about the others. + +## Context + +Guards need to know who's asking. Every Resource has access to `Graphiti.context` via the `#context` method (`lib/graphiti/resource.rb`). In a Rails app, including `Graphiti::Rails::Context` in your controller wraps every action in `Graphiti.with_context(graphiti_context, action_name.to_sym)`, and `graphiti_context` defaults to the controller instance itself (`lib/graphiti/rails/context.rb`): + +```ruby +class ApplicationController < ActionController::Base + include Graphiti::Rails::Context +end +``` + +That means `context` is your controller, and `context.current_user` (or whatever helper method your controller exposes) is available inside any Resource. Outside of Rails, set context manually: + +```ruby +Graphiti.with_context(OpenStruct.new(current_user: user)) do + PostResource.all +end +``` + +## Scoping records + +Override `#base_scope` to limit which records a Resource can ever return, regardless of filters: + +```ruby +class PostResource < ApplicationResource + def base_scope + Post.where(account_id: context.current_user.account_id) + end +end +``` + +This runs before filtering, sorting, and pagination, so it can't be bypassed by query params. It applies to sideloads too: a `has_many`/`belongs_to`/`has_one` on the related Resource inherits that Resource's `base_scope` unless the relationship itself passes an explicit `base_scope:` option (`lib/graphiti/sideload.rb#base_scope`), so a scoped Resource stays scoped no matter which relationship it's reached through. See [Composing with Scopes](/concepts/resources#composing-with-scopes) for how `base_scope` fits into the rest of query building. + +### Knowing which action you're in + +`base_scope` runs for every action, and sometimes you want it to behave differently for a collection than for a single record. Use `current_action`, which is the action name as a symbol. Rails sets it from `action_name` when wrapping the request (`lib/graphiti/rails/context.rb#wrap_graphiti_context`): + +```ruby +def base_scope + return Post.all if current_action == :show + Post.where(account_id: context.current_user.account_id) +end +``` + +Reach for this rather than digging through the query object's internals. `current_action` is public and stable. The params inside `Graphiti::Query` are neither. It is `nil` when nothing set it, which includes a Resource spec that calls `Graphiti.with_context` without a second argument. + +## Integrating with Pundit + +Graphiti has no built-in Pundit integration, but the two compose cleanly: let Pundit's policy scope decide which records exist, and let Graphiti guards decide which fields and relationships are exposed. + +Merge the policy scope in `base_scope`, so it can't be bypassed by query params: + +```ruby +class ApplicationResource < Graphiti::Resource + def base_scope + Pundit.policy_scope!(context.current_user, model) + end + + def current_user + context.current_user + end +end +``` + +Because `context` is the controller in Rails, per-record authorization stays where it always was: in the action, on the model the proxy hands you: + +```ruby +def show + post = PostResource.find(params) + authorize post.data + render jsonapi: post +end +``` + +Note the split: the policy scope answers "which records may appear at all", `authorize` answers "may this user see this specific record", and [attribute guards](#attribute-guards) answer "which fields of it". Reaching for a policy inside an attribute guard works too, since guards can receive the model: + +```ruby +attribute :salary, :integer, readable: :salary_visible? + +def salary_visible?(model) + Pundit.policy(current_user, model).salary? +end +``` + +## Attribute guards + +Pass a symbol, string, or proc to `readable:`/`writable:` on an attribute to gate it per-request. The guard method can optionally accept the model instance and the attribute name as arguments. Arity decides what it receives, and the model is only resolved if a guard actually declares a parameter for it (`lib/graphiti/util/attribute_check.rb`, `lib/graphiti/resource.rb#guard_model`): + +```ruby +class EmployeeResource < ApplicationResource + attribute :salary, :integer, writable: :salary_writable? + + def salary_writable?(model_instance, attribute_name) + context.current_user.admin? || context.current_user == model_instance.manager + end +end +``` + +On create the model is a new unsaved instance. On update it's the persisted record. A failed `writable` guard on a request rejects the write with an `unwritable_attribute` validation error before anything is persisted (`lib/graphiti/request_validators/validator.rb`). A failed `readable` guard omits the attribute from the response (`lib/graphiti/util/serializer_attributes.rb`). + +You can set the same guard for every attribute on a Resource with `attributes_readable_by_default`/`attributes_writable_by_default`, which also accept a symbol (`lib/graphiti/resource/configuration.rb`): + +```ruby +class ApplicationResource < Graphiti::Resource + self.attributes_writable_by_default = :writable_by_default? + + def writable_by_default?(model_instance, attribute_name) + PolicyChecker.new(context.current_user).writable?(model_instance, attribute_name) + end +end +``` + +Full option details, including `only`/`except` shorthand, live under [Limiting Behavior](/concepts/resources#limiting-behavior). + +## Relationship guards + +`has_many`, `belongs_to`, `has_one`, and `many_to_many` accept the same `readable:`/`writable:` guard shape, but unlike attribute guards, relationship guards take no arguments. Base the decision on `context` alone (`lib/graphiti/sideload.rb#evaluate_flag`): + +```ruby +class EmployeeResource < ApplicationResource + has_many :salary_histories, readable: :admin?, writable: :admin? + + def admin? + context.current_user.admin? + end +end +``` + +A failed `readable` guard silently scrubs the relationship from `?include=` before any records are fetched, and omits it from serialized output. A failed `writable` guard rejects a sidepost to that relationship with an `unwritable_relationship` validation error (`lib/graphiti/request_validators/validator.rb`). + +The guard method can live on either side of the relationship: Graphiti first looks for it on the resource declaring the relationship, falling back to the related resource if it isn't defined there (`lib/graphiti/sideload.rb#guard_resource`). That lets you define the guard once on the related resource and cover every relationship that points at it. + +To audit every guarded relationship across your app (useful before deploying a new guard), call `Graphiti.guarded_relationships`, which returns strings like `"EmployeeResource.salary_histories"` for every relationship whose `readable` or `writable` flag is a symbol, string, or proc (`lib/graphiti.rb#guarded_relationships`). + +See [Customizing Relationships](/concepts/relationships#customizing-relationships) for the rest of the relationship option surface. + +## Testing authorization + +Set context in a spec with `Graphiti.with_context`: + +```ruby +let(:ctx) { OpenStruct.new(current_user: double(admin?: true)) } + +it 'exposes salary to admins' do + Graphiti.with_context(ctx) { render } + expect(d[0].salary).to eq(100_000) +end +``` + +See [Context](/topics/testing#context) in the testing guide for more on setting context in Resource and API specs. diff --git a/docs/topics/caching.md b/docs/topics/caching.md new file mode 100644 index 00000000..7b9d4f21 --- /dev/null +++ b/docs/topics/caching.md @@ -0,0 +1,55 @@ +--- +title: 'Caching' +--- + +# Caching + +Graphiti can cache the rendered JSON response for a request, keyed off the underlying data and the parts of the query that affect rendering. This is opt-in at two levels: a global switch that enables cache-backed rendering, and a per-resource declaration that says which resources actually participate. + +## Enabling it + +First, tell Graphiti which cache store to use. Any object that responds to `fetch` works, so `Rails.cache` is the usual choice: + +```ruby +Graphiti.cache = Rails.cache +``` + +Then turn on cache-backed rendering globally: + +```ruby +Graphiti.configure do |config| + config.cache_rendering = true +end +``` + +If `cache_rendering` is `true` but `Graphiti.cache` isn't set to something that responds to `fetch`, Graphiti raises `"You must configure a cache store in order to use cache_rendering. Set Graphiti.cache = Rails.cache, for example."` the first time `Graphiti.config.cache_rendering?` is checked. + +`cache_rendering` alone doesn't cache anything, though. Each resource has to opt in with `cache_resource`: + +```ruby +class EmployeeResource < ApplicationResource + cache_resource expires_in: 5.minutes, tag: :cache_tag +end +``` + +`expires_in` defaults to `false` (no expiry) and `tag` defaults to `nil`. Calling `cache_resource` sets a resource-level flag that flows through every `all`/`find` call on that resource, so caching applies to both index and show-style requests. + +## What actually gets cached + +Only the rendered JSON is cached, not the database query. On render, if the resource proxy is cacheable and `Graphiti.config.cache_rendering?` is true, the renderer wraps the render call in `Graphiti.cache.fetch`, keyed by `"graphiti:render/#{proxy.cache_key}"`, versioned by `proxy.updated_at`, and expiring after `proxy.cache_expires_in`. If either condition is false, rendering happens normally with no cache involved. + +## Cache key composition + +`proxy.cache_key` combines three pieces, joined into a single expanded cache key: + +- **Scope cache key**: the underlying object's own `cache_key` (typically an ActiveRecord relation's `cache_key`, so it reflects the resolved records), combined with the `cache_key` of every sideloaded resource proxy. Sideloading `positions` or `department` folds their cache keys into the parent's. +- **Query cache key**: a SHA1 digest over the parts of the query that affect *rendering*: `extra_fields`, `fields`, whether links are requested, whether pagination links are requested, and `format`. Filters, sorts, and pagination page/size are deliberately not part of this digest. Two requests that select the same rendering options produce the same query cache key even if they filter different data. This is why the key is always combined with the scope key, which does vary with the resolved records. +- **Resource cache tag**: if `cache_resource` was given a `tag:`, and the resource responds to that method, its value is appended as a third segment (e.g. `cache_resource tag: :cache_tag` calls `resource.cache_tag` and appends the result). + +## Versioning and expiry + +`proxy.updated_at` is the max `updated_at` across the resolved records (`@object.maximum(:updated_at)`) and every sideloaded proxy's `updated_at`, recursively. If that calculation raises, Graphiti logs the error and falls back to `Time.now`, so a broken `updated_at` calculation degrades to "always fresh" rather than raising into the request. This value is passed as the cache store's `version:` option, so it participates in the effective cache entry the same way `ActiveSupport::Cache::Store#fetch` normally handles versioning. `expires_in` is passed straight through to the cache store as-is from `cache_resource`. + +## Debugging cache behavior + +When the [debugger](/topics/debugging) is enabled and a request's rendering is actually cached (`proxy.cached?` and `cache_rendering?` both true), the debug output includes a cache section showing the cache key's name, whether it's "stable" or "volatile" (based on how often the key changes across requests), and, when the key does change, which cache-key segments were added or removed. This is built on `Graphiti::Util::CacheDebug`, which persists hit/miss counts in `Graphiti.cache` between requests to compute those stats. See [ETags](/topics/etags) for the related per-response version identifier this same infrastructure computes. diff --git a/docs/topics/customizing-sideloads.md b/docs/topics/customizing-sideloads.md new file mode 100644 index 00000000..bded319d --- /dev/null +++ b/docs/topics/customizing-sideloads.md @@ -0,0 +1,156 @@ +--- +title: 'Customizing Sideloads' +--- + +# Customizing Sideloads +> [See the code in our sample app](https://github.com/graphiti-api/employee_directory/commit/e5dbb24b7e5853a9f39aed455a5d318d303df37e) + +This cookbook will help you understand sideloading. It would be great to +live in a world where everything follows default ActiveRecord table +conventions, but in my experience this is rarely the case. From legacy +code to alternate datastores, we need to think in Real World terms. + +Our [Employee Directory](https://github.com/graphiti-api/employee_directory) sample application +has a clean schema - let's screw with it. Let's say `Department` has a column called `watcher_emails`, which is an array of strings. We want to sideload `Department > Watchers`. Though the *relationship* is called `watchers`, these will be `Employee` records. + +Let's start by adding a spec: + +```ruby +# spec/resources/department/reads_spec.rb + +describe 'sideloading' do + describe 'watchers' do + let!(:employee1) { create(:employee) } + let!(:employee2) { create(:employee) } + let!(:employee3) { create(:employee) } + let!(:department) do + create :department, + watcher_emails: [employee1.email, employee3.email] + end + + before do + params[:include] = 'watchers' + end + + it 'sideloads employees via watcher_emails' do + render + sl = d[0].sideload(:watchers) + expect(sl.map(&:id)).to eq([employee1.id, employee3.id]) + expect(sl.map(&:jsonapi_type).uniq).to eq(['employees']) + end + end +end +``` + +Add the relationship: + +```ruby +# app/resources/department_resource.rb +has_many :watchers, resource: EmployeeResource +``` + +Run the test and you'll get this error: + +```error +Graphiti::Errors::AttributeError: + EmployeeResource: Tried to filter on attribute :department_id, but could not find an attribute with that name. +``` + +How would we track down this error? Well, we know Resources connect +together with [Links](/concepts/links). Let's +take a look at the query parameters that would be used to connect these +two Resources: + +```ruby +has_many :watchers, resource: EmployeeResource do + params do |hash, departments| + binding.pry + end +end +``` + +> Note - we're using [pry](https://github.com/pry/pry) to debug here. + +The value of `hash` here is: + +```ruby +{ filter: { department_id: "1" } } +``` + +Which makes sense. If we say `has_many :things`, by default we expect `Thing` to have a `department_id` we can query. + +That's not our case, though. Instead, let's customize those parameters +to fit our use case: + +```ruby +params do |hash, departments| + emails = departments.map(&:watcher_emails).flatten + hash[:filter] = { email: emails } +end +``` + +Instead of querying by `department_id`, we need to query by `email`. And +the value we pass in will be an array of email addresses + +We'd need to add an `email` filter to `EmployeeResource` to make this +work. This gets us ***querying*** correctly, but there's another error: + +```error +undefined method `department_id' for # +``` + +Here's the thing to keep in mind: let's say our request was +`/departments?include=watchers`. We queried all the data, and we now have an array of `Department`s and an array of `Employee`s. Now we need +to specify which employees should be assigned as watchers of which +department. + +Let's write that code manually: + +```ruby +has_many :watchers, resource: EmployeeResource do + # ... code ... + assign do |departments, employees| + departments.each do |d| + d.watchers = employees.select do |e| + e.email.in?(d.watcher_emails) + end + end + end +end +``` + +We're selecting all relevant `Employee`s for a given `Department` by checking the array of `watcher_emails`. + +This code can be tightened up a little with `assign_each` (recommended). +This way we don't have to iterate departments or worry about the +assignment ourselves: + +```ruby +has_many :watchers, resource: EmployeeResource do + # ... code ... + + assign_each do |department, employees| + employees.select { |e| e.email.in?(d.watcher_emails) } + end +end +``` + +We're using `#select` to return an array of relevant `Employee`s. If this was a `belongs_to` or `has_one` relationship, we'd probably want to use `#find` to return a single `Employee`. + +OK there's *one last error*: + +```error +undefined method `watchers=' for # +``` + +This one is simple - the `assign` function will call your Adapter's assignment logic, which by default will be a simple `department.watchers += relevant_employees`. That means we need to add a getter/setter for +this property: + +```ruby +# app/models/department.rb +attr_accessor :watchers +``` + +And we're done! The test should now pass. [Check out the working code +here](https://github.com/graphiti-api/employee_directory/tree/customize_sideloads_cookbook). diff --git a/docs/topics/debugging.md b/docs/topics/debugging.md new file mode 100644 index 00000000..c7179b0b --- /dev/null +++ b/docs/topics/debugging.md @@ -0,0 +1,242 @@ +--- +title: 'Debugging' +--- + +## Debugger {#debugger} + +Graphiti comes with a debugger that shows the queries executed for a +given request. Remember that Resources [have a query interface](/concepts/resources#query-interface) independent of a request or response. And Resources connect similar to ActiveRecord's `includes`: + +```ruby +employees = EmployeeResource.all +PositionResource.all(filter: { employee_id: employees.map(&:id) }) +``` + +> *Remember, this is all [customizable](/concepts/relationships#customizing-relationships)*. + +That means we can log the requests made by individual Resources: + +`/api/v1/employees?include=notes,positions.department.teams` +

+ +

+ +And even copy/paste these queries into a console session to debug: + +```bash +$ bin/rails c +>> TeamResource.all({:filter=>{:department_id=>"1,2,3"}}) +``` + +If you're having trouble with a request, see if you can isolate to a +specific Resource, then test that Resource directly. + +Finally: if an error occurs, we'll note the query that caused it: + +

+ +

+ +### JSON Output {#json-output} + +It can be helpful to have this debug output come back as part of the +JSON response. To enable this: + +```ruby +# app/controllers/application_controller.rb +def allow_graphiti_debug_json? + true + # or, current_user.admin? + # or, Rails.env.development? +end +``` + +And request the debug output: + +`/your/url?debug=true` + +You should now see the debug output in `meta`: + +

+ +

+ +
+ +If there's an error, and you've [enabled raw errors](/topics/error-handling#displaying-raw-errors), you'll also see the query that caused the error in the JSON response: + +
+ +

+ +

+ +
+ +### Configuration {#configuration} + +By default, we'll log to `Rails.logger`, and only enable debugging (logs or JSON) when `Rails.logger.level` is set to `debug`. Here are the +various ways to configure. + +Use `config.debug` to explicitly toggle debugging: + +```ruby +# config/initializers/graphiti.rb +Graphiti.configure do |c| + c.debug = false +end + +# Or use environment variable +# GRAPHITI_DEBUG=false +``` + +Use `config.debug_models` to get additional (but verbose) output: + +

+ +

+ +```ruby +# config/initializers/graphiti.rb +Graphiti.configure do |c| + c.debug_models = true +end + +# Or use environment variable +# GRAPHITI_DEBUG_MODELS=true +``` + +As noted above, `allow_graphiti_debug_json?` must return `true` if you +want JSON output: + +```ruby +# app/controllers/application_controller.rb +def allow_graphiti_debug_json? + true + # or, current_user.admin? + # or, Rails.env.development? +end +``` + +Note you need to explicitly pass `?debug=true` in the request. + +Assign a different logger: + +```ruby +Graphiti.logger = Logger.new(...) + +# Or the built-in STDOUT logger: +Graphiti.logger = Graphiti.stdout_logger +``` + +Manually apply the debugging (when using Rails, this normally happens in +a `around_action`): + +```ruby +Graphiti::Debugger.debug do + EmployeeResource.all +end +``` + +### Rake Tasks {#rake-tasks} + +There are some common debugging scenarios that are possible to do +manually, but their frequency warrants common patterns. For these, we +have rake tasks. + +#### graphiti:request {#graphiti-request} + +> `bin/rake graphiti:request[PATH,DEBUG]` + +Execute a request using `ActionDispatch::Integration::Session` (which +underlies request specs). + +This can be helpful when you don't have, or don't want to spin up, a web +server. Imagine you want to debug something on production, so you shell +into a docker container and edit some files locally. Now you want to +execute a request and see if your changes worked: + +```bash +$ bin/rake graphiti:request[/employees] +``` + +Will execute the request and spit out the JSON response. You may want to +run with the Debugger enabled: + +```bash +$ bin/rake graphiti:request[/employees,true] +``` + +Which add Debugger output as well. + +The `PATH` should not contain the domain unless you want to hit a live +API instead of a test server. + +#### graphiti:audit {#graphiti-audit} + +> `bin/rake graphiti:audit` + +Audits every relationship declared across your resources. It reports anything that will raise at request time, relationships that load an association just to render resource ids, and `belongs_to` relationships that render no ids unless included. A checklist at the end shows what was checked: + +``` +ERROR will raise when the relationship is included: the model has no association method + + EmployeeResource + has_many :positions Employee has no #positions method + + fix: define it, point the relationship at the real association with `as:`, or remove the relationship + +checks + + ✓ all relationships inspectable + ✗ 1 association method missing + ✓ all readable guards defined + ✓ all sideload filters declared + +graphiti: 12 resources, 40 relationships, 1 error. +``` + +The task exits nonzero when there are errors, so it can hold the line in CI. Run it before and after flipping [belongs_to_resource_ids_by_default](/concepts/relationships#belongs-to-resource-ids) to see exactly what the setting changes. + +#### graphiti:benchmark {#graphiti-benchmark} + +> `bin/rake graphiti:benchmark[PATH,NUM_REQUESTS]` + +It can be helpful to run a quick benchmark without hitting a live web +server, to eliminate the vagaries of latency. To do this: + +```bash +$ bin/rake graphiti:benchmark[/employees,100] +``` + +Which will return the average response time. + +#### Authorization headers {#Authorization-headers} + +If you have an Authorization scheme implemented (for example [authenticate_or_request_with_http_token](https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html) in rails) you can supply the `Authorization` http header value with the `AUTHORIZATION_HEADER` environment variable: + +```bash +$ export AUTHORIZATION_HEADER="Token --PRIVATE_API_KEY--" +$ bin/rake graphiti:request[/employees,true] +``` + +This also will work for `Basic` ([request_http_basic_authentication](https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Basic.html$$)) and `Bearer` values + +## Tips {#tips} + +When debugging an application, try to isolate the individual Resource +call and debug the Resource directly (instead of running the entire +request). This helps eliminate variables, and plain ruby code is easier +to work with. If possible, try to remove Graphiti entirely and focus on +your Models and Backends. + +The most common scenario is debugging a query. We suggest overriding +`resolve` and using [pry](https://github.com/pry/pry) (or equivalent): + +```ruby +# Introspect the scope without firing a query +# Call 'super' to fire the query +def resolve(scope) + binding.pry +end +``` diff --git a/docs/topics/error-handling.md b/docs/topics/error-handling.md new file mode 100644 index 00000000..20e39578 --- /dev/null +++ b/docs/topics/error-handling.md @@ -0,0 +1,279 @@ +--- +title: 'Error Handling' +--- + +## Overview {#overview} + +Whenever we have an application error, we want to respond with a +[JSONAPI-compliant errors payload](http://jsonapi.org/format/#errors). +This way clients have a predictable response detailing information about +the error. + +```json +{ + "errors": [ + { + "code": "internal_server_error", + "status": "500", + "title": "Internal Server Error" + } + ] +} +``` + +We'll also need a way to customize this payload. For instance, if a +`NotAuthorized` error is raised, the response should have a `403` status +code. For other errors, we may want to render a helpful error message: + +```ruby +class ApplicationController < ActionController::API + register_exception NotAuthorized, status: 403 + register_exception ShipmentDelayed, + detail: ->(e) { "Contact us at 123-456-7899" } + # ... code ... +end +``` + +Exception handling lives in Graphiti's Rails integration. Customizing the behavior based on error class happens in the [RescueRegistry](https://github.com/wagenet/rescue_registry) dependency. + +### Setup {#setup} + +Include the Rails integration in the controllers serving your resources: + +```ruby +class ApplicationController < ActionController::Base + include Graphiti::Rails::Controller +end +``` + +That registers handlers for Graphiti's own exceptions and renders anything else as JSON:API. `register_exception` itself is available on every controller without it. See below. + +#### Displaying Raw Errors {#displaying-raw-errors} + +When raw errors are on, the same payload carries the underlying exception under `meta.__raw_error__`: + +```json +{ + "errors": [ + { + "code": "internal_server_error", + "status": "500", + "title": "Internal Server Error", + "meta": { + "__raw_error__": { + "message": "EmployeesController::SomeError", + "backtrace": [ + "app/controllers/employees_controller.rb:5:in `index'", + "..." + ] + } + } + } + ] +} +``` + + +It can be useful to display the raw error as part of the JSON response - +but you probably don't want to expose your stack trace to customers. +Let's only show raw errors for the `staging` environment: + +```ruby +class ApplicationController < ActionController::API + # ... code ... + + def show_detailed_exceptions? + Rails.env.staging? + end +end +``` + +Another common pattern is to only show raw errors when the user is +privileged to see them: + +```ruby +class ApplicationController < ActionController::API + # ... code ... + + def show_detailed_exceptions? + current_user.admin? + end +end + +``` + +When `#show_detailed_exceptions?` returns `true`, you'll get the raw error class, +message, and backtrace in the JSON response. + +## Usage {#usage} + +### Basic {#basic} + +Let's register an error with a custom response code: + +```ruby +register_exception Errors::NotAuthorized, status: 403 +``` + +Now if we `raise Errors::NotAuthorized`, the response code will be `403`. + +Additional options: + +```ruby +register_exception Errors::NotAuthorized, + status: 403, + title: "You cannot perform this action", + detail: :exception, # render the raw error message + detail: ->(error) { "Invalid Action" } # message via proc +``` + +[See full documentation in the RescueRegistry README](https://github.com/wagenet/rescue_registry). + +All controllers will inherit any registered exceptions from their parent. They can also add their own. In this example, `FooError` will only throw a custom status code when thrown from `FooController`: + +```ruby +class FooController < ApplicationController + register_exception FooError, status: 422 +end +``` + +### Replacing Graphiti's own registrations {#replacing} + +Registering one of Graphiti's own errors again replaces its entry, and the last call wins. Keep the include above your own: + +```ruby +class ApiController < ActionController::API + include Graphiti::Rails::Controller + + register_exception Graphiti::Errors::UnsupportedPageSize, status: 422 +end +``` + +### Titles and details {#copy} + +Title and detail come from a locale key named after the error code: + +```yaml +en: + graphiti: + errors: + internal_server_error: + title: "Something went wrong" + detail: "We've probably received an error report already, but please contact us if the issue persists." + not_found: + title: "Not found" +``` + +`register_exception`'s own `title:` or `detail:` wins, and Graphiti registers its own client errors with `detail: :exception` so each reports the specific problem. With no key, the title is the HTTP status name and there is no detail. + +Validation messages are keyed the same way, by the code the payload reports in `meta.code`: + +```yaml +en: + graphiti: + errors: + format: "%{attribute} %{message}" + messages: + missing: "is missing" + invalid: "must be an object" + invalid_relationship: "is not a valid relationship" + unwritable_relationship: "cannot be written" + unknown_attribute: "is an unknown attribute" + unwritable_attribute: "cannot be written" + type_error: "should be type %{type}" + attribute_mismatch: "does not match the server endpoint" +``` + +`rails g graphiti:locale` writes that file, and `graphiti:install` calls it for you. + +A message goes into `meta.message` bare, and `format` joins it to the attribute for `detail`. Where that word order does not suit, a message can name its own `%{attribute}`. Translations hold up inside concurrent sideloads, since `I18n.locale` travels to the pool threads. + +### Error reporting {#error-reporting} + +Graphiti's client errors are in Rails' `rescue_responses`, so a 400 or 404 renders without being reported to `Rails.error` as an unhandled failure. Rails still logs them, and everything else is reported as before. + +This only changes what `Rails.error` hears about. An error tracker with its own middleware still catches everything, so you filter there too. + +Exceptions you register yourself are not in there, so a 403 of your own still counts as a failure. Name it the same way Rails names its own: + +```ruby +# config/application.rb +config.action_dispatch.rescue_responses["MyApp::Forbidden"] = :forbidden +``` + +To go the other way and hear about one of Graphiti's, drop it in an initializer, which runs after the railtie that installs them: + +```ruby +# config/initializers/graphiti.rb +ActionDispatch::ExceptionWrapper.rescue_responses.delete("Graphiti::Errors::RecordNotFound") +``` + +### Advanced {#advanced} + +The final option `register_exception` accepts is `handler`. Here you can inject your own error handling class that customize `RescueRegistry::ExceptionHandler`. For example: + +```ruby +class MyCustomHandler < Graphiti::Rails::ExceptionHandler + # self.exception accessible within all instance methods + + def status_code + # ...customize... + end + + def error_code + # ...customize... + end + + def title + # ...customize... + end + + def detail + # ...customize... + end + + def meta + # ...customize... + end +end + +register_exception FooError, handler: MyCustomHandler +``` + +If you would like to use the same custom handler for all errors, override `default_exception_handler`: + +```ruby +# app/controllers/application_controller.rb +def self.default_exception_handler + MyCustomHandler +end +``` + +## Testing {#testing} + +This pattern of globally rescuing exceptions makes sense when +running our live application...but during testing, we may want to +raise real errors and bypass this rescue logic. + +This is why we turn off error-handling during tests by default: + +```ruby +# spec/rails_helper.rb +RSpec.configure do |config| + config.include Graphiti::Rails::TestHelpers + # ... code ... + + config.before :each do + handle_request_exceptions(false) + end +end +``` + +If you want to turn this on for an individual test (so you can test +error codes, etc): + +```ruby +before do + handle_request_exceptions(true) +end +``` diff --git a/docs/topics/etags.md b/docs/topics/etags.md new file mode 100644 index 00000000..e64ce556 --- /dev/null +++ b/docs/topics/etags.md @@ -0,0 +1,46 @@ +--- +title: 'ETags' +--- + +# ETags + +Every resource proxy can compute a weak ETag for its current result set via `proxy.etag`. It's a plain string. Graphiti doesn't wire up `If-None-Match` handling or send `304 Not Modified` responses itself, so using it for HTTP conditional requests is up to your controller (for example, with Rails' own `fresh_when`/`stale?`). + +## How it's computed + +`etag` is a weak ETag built from the same cache key used for [caching](/topics/caching), but the *versioned* one: + +```ruby +def etag + "W/#{ActiveSupport::Digest.hexdigest(cache_key_with_version.to_s)}" +end +``` + +`cache_key_with_version` combines the scope's versioned cache key (which folds in every sideloaded proxy's versioned cache key and the underlying object's own `cache_key_with_version`), the query's cache key, and the resource cache tag if one is configured. Those are the same three ingredients described in the caching doc, except the scope portion here is version-aware rather than the plain identity-only key. In practice this means the ETag changes whenever the resolved records' `updated_at` values change, or whenever the rendering-relevant query params (fields, extra_fields, links, pagination_links, format) change. + +Because it's derived purely from `cache_key_with_version`, calling `etag` twice on equivalent proxies (same resource, scope, and query) produces the same weak ETag, and it's always prefixed with `W/`. + +## Using it + +Since there's no built-in controller integration, you compute and use it explicitly: + +```ruby +def index + employees = EmployeeResource.all(params) + response.headers["ETag"] = employees.etag + render jsonapi: employees +end +``` + +Or combine it with Rails' conditional-GET support if you want automatic `304` handling: + +```ruby +def index + employees = EmployeeResource.all(params) + fresh_when(etag: employees.etag) +end +``` + +## Relationship to resource-level caching + +`etag` doesn't require `cache_resource` or `Graphiti.config.cache_rendering = true`. It's available on any resource proxy regardless of whether that resource participates in rendering caching. It does, however, share its key ingredients with the cache-rendering machinery: the same `cache_key_with_version` that ETags are hashed from is also what `Graphiti::Util::CacheDebug` tracks (as `current_version[:etag]` / `last_version[:etag]`) when the [debugger](/topics/debugging) reports on cache-key changes for a cached resource. So if you're seeing an ETag change unexpectedly, the debugger's cache section (enabled the same way as for [caching](/topics/caching)) will show you which cache-key segment changed. diff --git a/docs/topics/hopping-relationships.md b/docs/topics/hopping-relationships.md new file mode 100644 index 00000000..f918bd2c --- /dev/null +++ b/docs/topics/hopping-relationships.md @@ -0,0 +1,149 @@ +--- +title: 'Hopping Relationships' +--- + +# Hopping Relationships +> [See the code](https://github.com/graphiti-api/employee_directory/commit/b187127d60ea67ef4c2a326721caeaad21ed7ec9) + +Our [sample application](https://github.com/graphiti-api/employee_directory) +has the setup `Employee > Position > Department`, where one of the positions is the `current_position`. What if we wanted to change this to `Employee > Department`, hiding everything about positions +under-the-hood? + +Let's start by saying an `Employee` has many `Department`s. Here's the +spec: + +```ruby +describe 'sideloading' do + describe 'departments' do + let!(:employee) { create(:employee) } + let!(:position1) do + create :position, + historical_index: 2, + employee: employee, + department: department1 + end + let!(:position2) do + create :position, + historical_index: 1, + employee: employee, + department: department2 + end + let!(:department1) { create(:department) } + let!(:department2) { create(:department) } + + before do + params[:include] = 'departments' + end + + it 'finds the departments for all positions' do + render + sl = d[0].sideload(:departments) + expect(sl.map(&:id)).to eq([department1.id, department2.id]) + expect(sl.map(&:jsonapi_type).uniq).to eq(['departments']) + end + end +end +``` + +Start by defining the association: + +```ruby +has_many :departments +``` + +And you'll get this error: + +```error +Graphiti::Errors::AttributeError: + DepartmentResource: Tried to filter on attribute :employee_id, but could not find an attribute with that name. +``` + +Which makes sense - if this is a `has_many` association, we'd expect DepartmentResource to filter by `employee_id`. Though in our case we +don't have that as a foreign key, we can still implement the +`employee_id` filter: + +```ruby +filter :employee_id, :integer, only: [:eq] do + eq do |scope, value| + scope.joins(:positions).merge(Position.where(employee_id: value)) + end +end +``` + +In order to find `Department`s by an `employee_id`, we need to join the `positions` table which has the `employee_id` column. + +We now get this error: + +```error +NoMethodError: + undefined method `employee_id' for # +``` + +Let's say our URL is `/employees?include=departments`. We've fetched all the `Employee`s and all the `Department`s, now we need to associate each `Department` with its relevant `Employee`. Normally we'd do that by looking at the `employee_id` foreign key on `Department`, but this +scenario has non-standard logic. Let's tell Graphiti how to select +relevant `Department`s for a given `Employee`: + +```ruby +has_many :departments do + assign_each do |employee, departments| + departments.select do |d| + employee_ids = d.positions.map(&:employee_id).flatten + employee.id.in?(employee_ids) + end + end +end +``` + +There's one final step - because we're assigning a department to an +employee, we have to make sure that accessor exists: + +```ruby +# app/models/employee.rb +attr_accessor :department +``` + + +And that's it! Our test now passes. + +There's a little bit of sleight-of-hand above though. Our filter joins +to the `positions` table, and our assignment iterates over departments and calls `department.positions`. **If we don't eager load, we'll cause +an N+!**! + +There are two solutions to this. The first is to simple change `.joins` to `.eager_load`: + +```ruby +scope.eager_load(:positions).merge(Position.where(employee_id: value)) +``` + +This ensures that not only are we joining on the `positions` table, we'll eagler load the `positions` *relationship* and avoid the N+1. + +If you're a stickler, though, you may have a nitpick. For one, if +we're hitting `/departments?filter[employee_id]` directly there is no need to eager load `positions` because we're never associating to an `Employee`. We're paying a performance penalty when we don't have to. + +OK, let's keep our filter `.joins`. We just have to tell Graphiti to switch it to `.eager_load` when sideloading through `EmployeeResource`: + +```ruby +has_many :departments do + # ... code ... + + pre_load do |proxy, employees| + proxy.scope.object = proxy.scope.object.eager_load(:positions) + end +end +``` + +The `pre_load` hook fires after we've built up the scope, but before we resolve it (before actually firing the query). It yields a `proxy` +object that we can modify - here we're modifying the scope to eager load +positions. + +It's up to you if you care about this scenario - you may want to start +with `.eager_load` and only embrace to the extra work of `pre_load` when +you really need it. + +The trick to these customizations is to think in Links. Resources +connect to each other with URLs - what would the query parameters of the +URL be? In this case, `filter?[employee_id]=123`. After that, we just +have to define how to associate relevant objects. Even with complex +associations hopping several levels, the same logic applies. + +See the final code [here](https://github.com/graphiti-api/employee_directory/commit/b187127d60ea67ef4c2a326721caeaad21ed7ec9). diff --git a/docs/topics/json-attributes.md b/docs/topics/json-attributes.md new file mode 100644 index 00000000..2e9501a9 --- /dev/null +++ b/docs/topics/json-attributes.md @@ -0,0 +1,77 @@ +--- +title: 'JSON Attributes' +--- + +# JSON Attributes + +Graphiti has two built-in types for structured data: `hash` and `array`. Both are useful for serving JSON/JSONB columns (or any nested data) through a Resource, without you writing custom typecasting. + +For the full type table, see [Types](/concepts/resources#types). This page covers `hash` and `array` specifically. + +## Declaring the attribute {#declaring} + +```ruby +class PostResource < ApplicationResource + attribute :metadata, :hash + attribute :tags, :array +end +``` + +Like any attribute, this is readable, writable, sortable and filterable by default. If your model reads a `metadata` JSONB column and returns a Ruby `Hash`, `attribute :metadata, :hash` will render it as-is. + +## Coercion rules {#coercion} + +Each type is a [Dry::Types](https://dry-rb.org/gems/dry-types) triple of `params` (used for filtering/sorting from query strings), `read`, and `write`. Per `lib/graphiti/types.rb`: + +* `hash` - `read` and `write` are `Dry::Types["strict.hash"]`. Nothing is coerced beyond requiring a real `Hash`. `params` is a custom type that runs `JSON.parse(input) if input.is_a?(String)` before validating with `Dry::Types["params.hash"]`, so a JSON string arriving in a query param gets parsed automatically. +* `array` - `read`, `write`, and `params` are all `Dry::Types["strict.array"]`. There is no `.of(...)` constraint, so elements are not individually coerced. Any array (including an array of hashes) passes through as-is. + +Both types have `kind: "record"` (`hash`) or `kind: "array"` (`array`) rather than `"scalar"`. One consequence: unlike every other base type (`integer`, `string`, `date`, etc.), `hash` and `array` do **not** get an `array_of_*` doppelgänger generated (`lib/graphiti/types.rb` explicitly excludes `:boolean`, `:hash`, and `:array` when building `array_of_*` variants). If you need an array of hashes, just use `attribute :things, :array` - there's no `array_of_hashes` type. + +On coercion failure - reading, writing, or filtering - Graphiti raises `Graphiti::Errors::TypecastFailed` with the attribute name, the offending value, and the underlying error. + +## Filtering on a hash attribute {#filtering} + +Declaring `attribute :metadata, :hash` makes it filterable with the `eq` operator by default (the `hash` type only supports `eq` out of the box, per the default operator map). A request like: + +``` +GET /posts?filter[metadata]={"status":"draft"} +``` + +parses the JSON string into a Ruby `Hash` before your filter block runs: + +```ruby +filter :metadata, :hash do + eq do |scope, value| + # value => [{ "status" => "draft" }] + scope + end +end +``` + +Note the value is wrapped in an array - Graphiti's filter pipeline supports passing multiple comma-separated JSON objects (`filter[metadata]={"a":1},{"b":2}`), so `eq` always receives an array of hashes unless you opt out. + +Pass `single: true` to receive the hash directly instead of an array-wrapped one, and to skip the comma-splitting behavior entirely (useful once your hash values might legitimately contain commas): + +```ruby +filter :metadata, :hash, single: true do + eq do |scope, value| + # value => { "status" => "draft" } + scope + end +end +``` + +A Ruby `Hash` (rather than a JSON string) passed directly as a filter param works the same way. It's validated rather than parsed. + +Array attributes filter similarly: `filter[tags]=ruby,rails` splits on commas into `["ruby", "rails"]`. Wrap a value in `{{curlies}}` to prevent comma-splitting (see [Escaping Values](/concepts/resources#escaping-values)). + +## Writing to a JSON column {#writing} + +There's nothing Graphiti-specific to do here. On a write request, Graphiti coerces the incoming JSON attribute through the `write` type (`strict.hash` or `strict.array` - just a presence/type check) and assigns it to your model via `attributes[:metadata] = value`. Persisting that Ruby `Hash`/`Array` into an actual `jsonb`/`json` column is entirely up to your ORM (ActiveRecord serializes it automatically for `jsonb`/`json` columns) - Graphiti does not serialize to a JSON string itself, so don't do that in your own code either or you'll end up double-encoded. + +## Caveats {#caveats} + +* `hash` and `array` only support the `eq` filter operator by default - there's no built-in `gt`/`lt`/`prefix` for structured data. Add custom operators yourself if you need them. +* Non-`single` hash filters always hand your `eq` block an array, even for a single JSON object - a common source of confusion is forgetting the `value[0]` unwrap. +* There's no schema validation built in - `strict.hash`/`strict.array` just confirm you got a `Hash`/`Array`, not that its keys match anything in particular. For a shape check, register a [custom type](/concepts/resources#custom-types) with `Dry::Types["hash"].schema(...)`. diff --git a/docs/topics/openstruct-models.md b/docs/topics/openstruct-models.md new file mode 100644 index 00000000..ca442ba4 --- /dev/null +++ b/docs/topics/openstruct-models.md @@ -0,0 +1,50 @@ +--- +title: 'OpenStruct Models' +--- + +# OpenStruct Models + +[Model Requirements](/concepts/backends-and-models#model-requirements) covers what any Model needs to respond to, and [Usage Without ActiveRecord](/topics/without-activerecord) walks through building a Resource around a PORO. `OpenStruct` satisfies those requirements with zero boilerplate - no `attr_accessor` list, no constructor - which is exactly why Graphiti uses it internally for [remote resources](/topics/remote-resources): `Resource::Remote` and the default `Sideload` model both set `self.model = OpenStruct` (`lib/graphiti/resource/remote.rb`, `lib/graphiti/sideload.rb`), since a remote resource doesn't know its shape ahead of time. That convenience comes with sharp edges if you reach for `OpenStruct` as a model in your own Resources. + +## What Graphiti expects from it {#expectations} + +Reads go through `@object.send(attribute_name)` (`lib/graphiti/util/serializer_attributes.rb`), and writes go through `model.send(:"#{key}=", value)`-style assignment. `OpenStruct` handles both via `method_missing`, so any attribute you construct it with - or assign later - just works, same as a PORO with `attr_accessor`. + +## The gotcha: typos and reserved methods return silently, they don't raise {#gotcha} + +An `attr_accessor`-based PORO raises `NoMethodError` the moment you call an undefined reader. `OpenStruct` does not - if the attribute was never set, `#send` on it just returns `nil`: + +```ruby +require "ostruct" +o = OpenStruct.new(name: "a") +o.send(:naem) # => nil, not NoMethodError +``` + +Since attribute reads happen inside `@object.send(name_ref)`, a typo'd attribute name (in your `attribute` declaration, or a rename you forgot to propagate) will silently serialize as `null` instead of blowing up in your test suite. With a real PORO the same typo raises immediately and is easy to catch. + +Worse, `OpenStruct` only overrides *undefined* methods - if the attribute name collides with something `Object`/`Kernel` already defines, the field is silently swallowed and you get the *original* method's return value instead of your data: + +```ruby +o = OpenStruct.new(hash: 123, count: 5) +o.hash # => some large integer (Object#hash), NOT 123 +o.count # => 5, fine - `count` isn't a reserved method +``` + +`id`, `class`, `object_id`, `hash`, `send`, `freeze`, and `to_s` are all real methods on every Ruby object. Naming an attribute after one of them (a `hash` field to store a checksum is a realistic trap given Graphiti's own `:hash` type) won't error - it'll quietly return the wrong value. `id` itself is safe (`Object#id` was removed from modern Ruby in favor of `#object_id`), but don't assume the rest are. + +## Validations {#validations} + +`OpenStruct` doesn't include `ActiveModel::Validations`, and the [Null adapter's `#save`](/concepts/backends-and-models#model-requirements) only calls `model.valid?` if the model `respond_to?(:valid?)` - so an unvalidated `OpenStruct` model will save "successfully" with no errors payload, not raise. If you want write-request validation, subclass it: + +```ruby +class Employee < OpenStruct + include ActiveModel::Validations + validates :first_name, presence: true +end +``` + +This works exactly as it would on any other class - `OpenStruct` doesn't get in the way of `include`. + +## When it's the right call {#when} + +`OpenStruct` is a reasonable choice for throwaway resources, prototypes, and cases like remote resources where the attribute set is genuinely dynamic. For a Resource you're going to maintain, prefer a real PORO, `ActiveModel::Model`, or `Dry::Struct` (all shown in [Model Implementations](/concepts/backends-and-models#model-implementations)) - you get the same zero-ORM flexibility with a class that fails loudly on a mistake instead of quietly serializing `nil`. diff --git a/docs/topics/remote-resources.md b/docs/topics/remote-resources.md new file mode 100644 index 00000000..d624ec30 --- /dev/null +++ b/docs/topics/remote-resources.md @@ -0,0 +1,291 @@ +--- +title: 'Remote Resources' +--- + +## Overview {#overview} + +Resources have a defined query contract, and connect together with [Links](/concepts/links). That contract doesn't care whether the sideloaded Resource lives in the same application, so we can point it at a separate service instead: + +```ruby +has_many :comments, + remote: 'http://blog-api.com/api/v1/comments' +``` + +Splitting an application into services tends to break down at the boundary between them: no consistent query interface, no consistent error handling, no types or backwards-compatibility checks. Graphiti was built to address exactly this - a defined query contract, an errors payload, and a schema with types and backwards-compatibility checks, all organized into RESTful Resources - so cross-service communication is automated rather than hand-rolled per integration. + +> Note: Remote Resources are for **read** operations only. The exception +> is associating to an existing `belongs_to` remote entity. + +> Note: We use [Faraday](https://github.com/lostisland/faraday) to hit +> the remote API. You must add `faraday` to your Gemfile to enable +> remote resources. + +### How it Works {#how-it-works} + +Let's take a simple association: + +```ruby +class PostResource < ApplicationResource + has_many :comments +end +``` + +This would generate a [Link](/concepts/links) for +lazy-loading comments: + +```ruby +{ + related: "http://my-api.com/api/v1/comments?filter[post_id]=123" +} +``` + +Critically, **those same lazy-loading parameters are used when +eager-loading**: + +```ruby +# under the hood +posts = PostResource.all.data +CommentResource.all(filter: { post_id: 123 }) +``` + +OK, and we also know Resources support [any backend](/concepts/backends-and-models), and we can build an [Adapter](/topics/without-activerecord#adapters) if our backend supports common operations like filtering, sorting, and pagination. + +So, that means we can build an Adapter that makes an HTTP request to another Graphiti Resource that lives in a separate API. That adapter is built into Graphiti and comes out-of-the-box: `Graphiti::Adapters::GraphitiAPI` + +```ruby +class CommentResource < ApplicationResource + self.remote = "http://my-api.com/api/v1/comments" + # under-the-hood, this sets: + # self.adapter = Graphiti::Adapters::GraphitiAPI +end +``` + +This Resource works as normal. We can execute queries: + +```ruby +comments = CommentResource.all({ + sort: '-id', + filter: { active: true } +}) + +# The model instances are OpenStructs +comments.data # => [#, #, ...] + +# Those models reflect all the properties returned from the API: +comments.data.map(&:author) # => ["Jane Doe", "John Doe", ...] +``` + +And we can sideload just like we always do: + +```ruby +class PostResource < ApplicationResource + # Nothing to see here! + has_many :comments +end +``` + +We'll still support Deep Querying - let's fetch the Post and its +active comments, ordered by `created_at`: + +`/posts?include=comments&sort=comments.created_at&filter[active]=true` + +Let's say `CommentResource` has an association to `Author`. If `AuthorResource` is defined in the remote API, we can fetch it as normal - no special configuration needed to fetch the `Post`, `Comment`s and `Author`s in a single request. + +But maybe only `CommentResource` is remote, and `Authors` are local. +We need only define the association locally: + +```ruby +class CommentResource < ApplicationResource + self.remote = "http://my-api.com/api/v1/comments" + + belongs_to :author +end +``` + +Let's say we need to tweak the display of a property coming from the +remote API. Again, works just like normal: + +```ruby +class CommentResource < ApplicationResource + self.remote = "http://my-api.com/api/v1/comments" + + attribute :body, :string do + @object.body.truncate(100) + end +end +``` + +You only need to define attributes when overriding this logic - +otherwise we'll take them directly from the API response. This means you +don't have to update two repos and coordinate deploys - as soon as you +add a property to the remote API and deploy it, it will be reflected in +the local API response. + +For the typical use case, we don't even *need* to create this Resource +class. The sideload definition accepts a `remote:` option, which will +create a Remote Resource under-the-hood: + +```ruby +class PostResource < ApplicationResource + has_many :comments, remote: 'http://my-api.com/api/v1/comments' +end + +# Equivalent to: +# +# class PostResource < ApplicationResource +# has_many :comments +# end +# +# class CommentResource < ApplicationResource +# self.remote = 'http://my-api.com/api/v1/comments' +# end +``` + +> NOTE: When sending a request to a remote API, we request page size +> `999` so results don't get accidentally cut off. If you need +> successive requests, please [submit an issue](https://github.com/graphiti-api/graphiti/issues). + +### Customizing {#customizing} + +We use [Faraday](https://github.com/lostisland/faraday) under-the-hood, +which allows for various adapters and middleware. In addition: + +#### Configure Timeout {#configure-timeout} + +```ruby +class CommentResource < ApplicationResource + self.remote = "..." + + # Customize faraday timeout + self.timeout = 10 + self.open_timeout = 20 +end +``` + +#### Configure Request {#configure-request} + +```ruby +class CommentResource < ApplicationResource + self.remote = "..." + + def make_request(url) + # request here is from Faraday: + # + # conn.get do |req| + # yield req + # end + # + super do |request| + request.headers["Custom"] = "Header" + end + end +end +``` + +#### Configure Headers {#configure-headers} + +By default we're going to *forward* the `Authorization` header of the request to the remote API. To override the default headers sent: + +```ruby +# app/resources/comment_resource.rb +def request_headers + { "Some-Foo" => "bar" } +end +``` + +### Error Handling {#error-handling} + +If the remote API has an error, we want to re-raise that same error. But +unless you've enabled [displaying raw errors](/topics/error-handling#displaying-raw-errors), we won't be able to - the only information we have is what's returned from the API. + +You're encouraged to display raw errors when an internal or privileged +user: + +```ruby +rescue_from Exception do |e| + handle_exception(e, show_raw_error: current_user.developer?) +end +``` + +If you do this, we'll be able to re-raise the original error, including +stacktrace. If raw errors are not enabled, we'll raise whatever +information is given. + +Both styles will be wrapped in `Graphiti::Errors::Remote`, so you can +differentiate between a local error and a remote one. + +## Testing {#testing} + +When testing a remote resource, we need to mock the API request and +response. Graphiti gives you a spec helper to do just that - +`include_context "remote api"`: + +```ruby +describe 'comments' do + include_context 'remote api' + + let(:api_response) do + { + data: [{ + id: '1', + type: 'comments', + attributes: { body: 'hello' } + }] + } + end + + it 'does something' do + url = 'http://my-api.com/api/v1/comments?page[size]=999' + mock_api(url, api_response) + # ... test ... + end +end +``` + +This shows all the pieces needed to test remote APIs. We want to test + +* The correct URL is hit +* When given a valid response, the rest of the flow works as expected. + +> NOTE: if the remote relationship is a has_many, the API will need to +> return the foreign key as part of the response. Otherwise, we won't +> know how to associate these children to their parents. + +Here's a slightly longer version, showing that `Post` can sideload `Comment`s: + +```ruby +describe 'sideloading' do + describe 'comments' do + include_context 'remote api' + + let!(:post) { create(:post) } + + let(:api_response) do + { + data: [{ + id: '789', + type: 'comments', + attributes: { body: 'hello' } + }] + } + end + + before do + params[:include] = 'comments' + end + + it 'does something' do + url = "http://my-api.com/api/v1/comments" + url += "?filter[post_id]=#{post_id}" + mock_api(url, api_response) + render + sl = d[0].sideload(:comments) + expect(sl.map(&:id)).to eq(['789']) + expect(sl.map(&:jsonapi_type).uniq) + .to eq(['comments']) + end + end +end +``` + +> Make sure to include `page[size]=999` in the test URL! diff --git a/docs/topics/testing.md b/docs/topics/testing.md new file mode 100644 index 00000000..426f7699 --- /dev/null +++ b/docs/topics/testing.md @@ -0,0 +1,931 @@ +--- +title: 'Testing' +--- + +## Overview {#overview} + +Test first. + +Wait, hear me out! + +[Even if you're not a fan of TDD](http://david.heinemeierhansson.com/2014/tdd-is-dead-long-live-testing.html), Graphiti *integration* tests are the easiest, most pleasant way to develop. In fact, most Graphiti development can happen without even opening a browser. And as a side effect, you get a reliable test suite. + +Let's say we want to filter Employees by `title`, which comes from the `positions` table. Start with a spec: + +```ruby +RSpec.describe EmployeeResource, type: :resource do + describe 'filtering' do + context 'by title' do + # GIVEN some seed data + let!(:employee1) { create(:employee) } + let!(:employee2) { create(:employee) } + let!(:position1) do + create :position, + title: 'foo', + employee: employee1 + end + let!(:position2) do + create :position, + title: 'bar', + employee: employee2 + end + + # WHEN a parameter is set + before do + params[:filter] = { title: 'bar' } + end + + # THEN the query results will be correct + it 'works' do + expect(records.map(&:id)).to eq([employee2.id]) + end + end + end +end +``` + +By developing test-first: + +* We don't need to struggle with seeding local development data or finding the right records for specific scenarios - we can seed randomized data on-the-fly with [factories](https://github.com/thoughtbot/factory_bot). +* There's no need to spin up a server and refresh browser pages, mentally parsing the response payload. +* We get a high-confidence test "for free". +* Because our integration test is separate from implementation, we don't need to worry about [test-induced design damage](http://david.heinemeierhansson.com/2014/test-induced-design-damage.html). + +### API vs Resource {#api-vs-resource} + +There are two types of Graphiti tests: **API tests** and **Resource tests**. + +This is because the same Resource logic can be re-used at multiple endpoints. PostResource can be referenced at `/posts`, `/top_posts`, and `/admin/posts`, but we shouldn't have to test the same filtering and sorting logic over and over. Querying, persistence, and serialization are all Resource responsibilities, tested in Resource tests. + +We still want API tests, though, to test everything outside of the Resource: routing, middleware, cache rules, response codes, etc… + +Typically, you'll write the API test **once** and not have to touch it again. + +### Factories {#factories} + +> Note: Factories are not **required**, but they are considered a best practice used by the Graphiti test generator. Read thoughtbot's [Why Factories?](https://robots.thoughtbot.com/why-factories) for more information. + +We need to seed data into our test database. To do this, we use [Factory Bot](https://github.com/thoughtbot/factory_bot) and [Faker](https://github.com/stympy/faker). + +When you generate a model, a stub factory will be created. It is highly recommended you edit that factory with randomized data: + +```ruby +# BEFORE +FactoryBot.define do + factory :employee do + first_name { 'MyString' } + end +end + +# AFTER +FactoryBot.define do + factory :employee do + first_name { Faker::Name.first_name } + end +end +``` + +This will help catch edge cases and provide more clarity than seeing the same `"MyString"` everywhere. + +It's a best practice that if a factory defines an attribute, there should be a corresponding validation around that attribute. If an attribute is optional, it should not be defaulted in a factory. + +Finally, Rails requires `belongs_to` associations by default. This means that if Employee `belongs_to :department`, then `create(:employee)` will fail. To ensure a relationship is always seeded: + +```ruby +FactoryBot.define do + factory :employee do + department + # OR association :department, factory: :department + end +end + +``` + +### RSpec Setup {#rspec} + +RSpec is not **required**, but considered a first-class citizen used by the Graphiti test generator. + +Add the following to your Gemfile: + +```ruby +# Gemfile +group :development, :test do + gem 'factory_bot_rails' + gem 'rspec_rails' + gem 'faker' +end + +group :test do + gem 'database_cleaner' +end +``` + +Bootstrap RSpec if you haven't already: + +```bash +$ bin/rails g rspec:install +``` + +Then wire up Graphiti's helpers and reset your database between examples: + +```ruby +require 'graphiti/spec_helpers/rspec' + +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods + config.include Graphiti::SpecHelpers::RSpec + config.include Graphiti::Rails::TestHelpers, type: :request + + # Clean your DB between test runs + config.before(:suite) do + DatabaseCleaner.strategy = :transaction + DatabaseCleaner.clean_with(:truncation) + end + + config.around(:each) do |example| + begin + DatabaseCleaner.cleaning do + example.run + end + ensure + DatabaseCleaner.clean + end + end +end +``` + +## Test Helpers {#test-helpers} + +Tests are run using [JSONAPI standards](http://jsonapi.org/format/#fetching-includes). But the JSONAPI payload can be a pain to deal with. So, we've supplied helpers. + +These helpers ship with Graphiti, under `Graphiti::SpecHelpers`. + +### #jsonapi_data {#jsonapi-data} + +The `jsonapi_data` method will parse response data and return a normalized object (`Graphiti::SpecHelpers::Node`). Assert against this the same way you assert against JSON: + +```ruby +data = jsonapi_data[0] +expect(data.id).to eq(employee.id) +expect(data.jsonapi_type).to eq('employees') +expect(data.first_name).to eq('Jane') +``` + +* `id` will automatically case to an integer. If you would like to avoid this, use `rawid` instead. +* `jsonapi_type` is a convenience method for `data/type`, to avoid conflicting with an attribute of the same name. +* If the `first_name` key was not present in the response, an error will be raised. + +#### Accessing Sideloads {#accessing-sideloads} + +To grab a relationship: + +```ruby +sideload = jsonapi_data[0].sideload(:comments) +expect(sideload.id).to eq(123) +expect(sideload.jsonapi_type).to eq('comments') +expect(sideload.body).to eq('body') +``` + +The `sideload` method accepts the *name of the relationship*. It returns a normal `jsonapi_data` `Graphiti::SpecHelpers::Node` containing the `include`-ed data. + +#### Accessing Links {#accessing-links} + +To grab a Link: + +```ruby +jsonapi_data[0].link(:comments, :related) +``` + +This accepts the relationship name and the link type. It will return the link URL. + +### #json {#json} + +To see the raw JSON response, use `json`. + +### #json_date and #json_datetime {#date-and-datetime} + +In Graphiti, datetimes are rendered in [ISO 8601 format](https://www.iso.org/iso-8601-date-and-time-format.html). This means that straight date comparisons will fail: + +```ruby +# WRONG +expect(jsonapi_data[0].created_at).to eq(post.created_at) +``` + +Instead, use the `json_datetime` helper to convert to ISO 8601 and compare apples to apples: + +```ruby +# RIGHT +expect(jsonapi_data[0].created_at).to eq(json_datetime(post.created_at)) +``` + +Similarly, there's a `json_date` helper as well. + +### #jsonapi_errors {#jsonapi-errors} + +To parse an [Errors Payload](http://jsonapi.org/format/#errors): + +```ruby +errors = jsonapi_errors + +# Direct access +expect(errors.length).to eq(1) +expect(errors[0].attribute).to eq(:name) +expect(errors[0].status).to eq('422') +expect(errors[0].title).to eq('Validation Error') +expect(errors[0].detail).to eq("Name can't be blank") +expect(errors[0].code).to eq(:blank) +expect(errors[0].message).to eq("can't be blank") + +# By attribute +expect(errors.name.message).to eq("can't be blank") +expect(errors.name.code).to eq(:blank) +# ... etc ... + +# As a hash +expect(errors.to_h).to eq({ + name: "can't be blank" +}) +``` + +### Resource Test Helpers {#resource-test-helpers} + +Resource tests have two helpers, both different ways to execute a query. + +`render` will fire the query and return a JSON response that can be accessed as normal: + +```ruby +it 'works' do + render + expect(jsonapi_data[0].first_name).to eq('Jane') + json # => { data: { type: 'employees', ... } } +end +``` + +`records` will return model instances: + +```ruby +it 'works' do + render + expect(records.map(&:id)).to eq([1, 2, 3]) +end +``` + +### Resource Matchers {#resource-matchers} + +For one-line assertions about a Resource's shape, use the built-in matchers. They're included automatically in `type: :resource` specs and expect a Resource instance as the subject: + +```ruby +RSpec.describe PostResource, type: :resource do + subject { described_class.new } + + it { is_expected.to belong_to_resource(:author) } + it { is_expected.to have_many_resources(:comments) } + it { is_expected.to have_one_resource(:detail) } + it { is_expected.to expose_attribute(:title, :string) } + it { is_expected.to filter_attribute(:title, :string) } +end +``` + +Each matcher accepts `with_options` to assert configuration: + +```ruby +it do + is_expected.to belong_to_resource(:author) + .with_options(foreign_key: :author_id, resource: AuthorResource) +end + +it { is_expected.to expose_attribute(:title, :string).with_options(writable: false) } +``` + +### API Test Helpers {#api-test-helpers} + +When executing an API test request, always use the `jsonapi_` doppelgänger: + +* `jsonapi_get(url, params:)` instead of `get` +* `jsonapi_post(url, payload)` instead of `post` +* `jsonapi_put(url, payload)` instead of `put` +* `jsonapi_patch(url, payload)` instead of `patch` +* `jsonapi_delete(url)` instead of `delete` + +This will set the `CONTENT_TYPE` header to `application/vnd.api+json` and call `to_json` on the payload (when applicable). + +It also allows overriding `jsonapi_headers`. Use this to manipulate headers for a given request: + +```ruby +def jsonapi_headers + super.tap do |headers| + headers['CUSTOM'] = 'foo' + end +end +``` + +### Guard Helpers {#guard-helpers} + +Many teams use [guard](https://github.com/guard/guard) in development to watch their project files and run a smaller set of focused tests as code changes. For those teams leveraging guard and the [guard-rspec plugin](https://github.com/guard/guard-rspec), we offer an additional set of DSL helpers via the [guard-rspec-graphiti plugin](https://github.com/graphiti-api/guard-rspec-graphiti). For more details, check out the [project README](https://github.com/graphiti-api/guard-rspec-graphiti/blob/master/README.md). + +## Resource Tests {#resource-tests} + +There are two test files for each Resource: + +* `spec/resources/post/reads_spec.rb` +* `spec/resources/post/writes_spec.rb` + +### Reads {#reads} + +The basic setup for read operations: + +```ruby +# spec/resources/employee/reads_spec.rb +require 'rails_helper' + +RSpec.describe EmployeeResource, type: :resource do + describe 'serialization' do + # ... code ... + end + + describe 'filtering' do + # ... code ... + end + + describe 'sorting' do + # ... code ... + end + + describe 'sideloading' do + # ... code ... + end +end +``` + +#### Serialization {#serialization} + +```ruby +describe 'serialization' do + let!(:employee) { create(:employee, first_name: 'Jane') } + + it 'works' do + render + data = jsonapi_data[0] + expect(data.id).to eq(employee.id) + expect(data.jsonapi_type).to eq('employees') + expect(data.first_name).to eq('Jane') + end +end +``` + +Best practices: + +* Assert on all attributes, even if there is no logic. This way adding logic will cause a test failure. +* When seeding data, manually assign values. This way you can be assured you aren't accidentally testing `nil == nil` + +If you decide you have a high level of confidence in your factories, you can instead save some keystrokes and assert on randomized data: + +```ruby +expect(data.first_name).to eq(employee.first_name) +``` + +> Note: Our schema validation test will ensure no attributes get removed or change types. + +#### Filtering {#filtering} + +```ruby +describe 'filtering' do + let!(:employee1) { create(:employee) } + let!(:employee2) { create(:employee) } + + context 'by id' do + before do + params[:filter] = { id: { eq: employee2.id } } + end + + it 'works' do + render + expect(jsonapi_data.map(&:id)).to eq([employee2.id]) + end + end +end +``` + +In general, you only need to test filtering when there is custom logic. Our schema validation test will ensure no filters are removed, guarded, changed operators, etc. + +#### Sorting {#sorting} + +```ruby +describe 'sorting' do + describe 'by id' do + let!(:employee1) { create(:employee) } + let!(:employee2) { create(:employee) } + + context 'when ascending' do + before do + params[:sort] = 'id' + end + + it 'works' do + render + expect(jsonapi_data.map(&:id)).to eq([ + employee1.id, + employee2.id + ]) + end + end + + context 'when descending' do + before do + params[:sort] = '-id' + end + + it 'works' do + render + expect(jsonapi_data.map(&:id)).to eq([ + employee2.id, + employee1.id + ]) + end + end + end +end +``` + +In general, you only need to test sorting when there is custom logic. Our schema validation test will ensure no sorts are removed, guarded or limited in direction. + +#### Sideloading {#sideloading} + +```ruby +describe 'sideloading' do + let!(:employee) { create(:employee) } + + describe 'current_position' do + let!(:pos1) do + create(:position, employee: employee, historical_index: 2) + end + let!(:pos2) do + create(:position, employee: employee, historical_index: 1) + end + + before do + params[:include] = 'current_position' + end + + it 'returns position with historical index == 1' do + render + sl = jsonapi_data[0].sideload(:current_position) + expect(sl.jsonapi_type).to eq('positions') + expect(sl.id).to eq(pos2.id) + end + end +end +``` + +There is no need to test each attribute of the sideload - this should be tested in the [Resource Test](#resource-tests) of the sideloaded Resource. + +In general, you only need to test sideloads when there is custom logic. Our schema validation test will ensure no sideloads are removed or associated to a different Resource. + +### Writes {#writes} + +The basic setup for write operations: + +```ruby +# spec/resources/employee/writes_spec.rb +require 'rails_helper' + +RSpec.describe EmployeeResource, type: :resource do + describe 'creating' do + let(:payload) { ... } + # ... code ... + end + + describe 'creating' do + let(:payload) { ... } + # ... code ... + end + + describe 'destroying' do + # ... code ... + end +end +``` + +Here `payload` is a [JSONAPI Resource Object](http://jsonapi.org/format/#crud). + +#### Create {#create} + +```ruby +describe 'creating' do + let(:payload) do + { + data: { + type: 'employees', + attributes: { } + } + } + end) + + let(:instance) do + EmployeeResource.build(payload) + end + + it 'works' do + expect { + expect(instance.save).to eq(true) + }.to change { Employee.count }.by(1) + end +end +``` + +`payload` starts as an empty Employee [Resource Object](http://jsonapi.org/format/#crud), asserting only that saving it creates an Employee. You'll likely want to add attributes here and ensure they are persisted correctly: + +```ruby +let(:payload) do + { + data: { + type: 'employees', + attributes: { first_name: 'Jane', age: 30 } + } + } +end + +# ... code ... + +it 'works' do + expect { + expect(instance.save).to eq(true) + }.to change { Employee.count }.by(1) + employee = Employee.last + expect(employee.first_name).to eq('Jane') + expect(employee.age).to eq(30) +end +``` + +##### Required Belongs To {#required-belongs-to} + +Rails requires `belongs_to` associations by default. This means that if Employee `belongs_to :department`, the above tests will fail (we cannot create the Employee without associating it to Department). + +You have 3 options here: + +* Turn off this validation in test mode. Add `config.active_record.belongs_to_required_by_default = false` to `config/environments/test.rb`. +* Turn off the validation for this specific relationship: `belongs_to :department, optional: true`. +* Associate as part of the request. + +We recommend the third option to preserve real-world end-to-end behavior: + +```ruby +describe 'creating' do + let!(:department) { create(:department) } + + let(:payload) do + { + type: 'employees', + attributes: { ... }, + relationships: { + department: { + data: { + type: 'departments', + id: department.id.to_s + } + } + } + } + end + + # ... code ... +end +``` + +This ensures the Employee is created and associated to the given department. + +#### Update {#update} + +An update spec looks like the create spec, but finds an existing record instead of building a new one, and asserts the changed attribute rather than a changed count: + +```ruby +describe 'updating' do + let!(:employee) { create(:employee) } + + let(:payload) do + { + data: { + id: employee.id.to_s, + type: 'employees', + attributes: { first_name: 'changed!' } + } + } + end + + let(:instance) do + EmployeeResource.find(payload) + end + + it 'works' do + expect { + expect(instance.update).to eq(true) + }.to change { employee.reload.updated_at } + .and change { employee.first_name }.to('changed!') + end +end +``` + +> Note that this test will be pending by default when using the generator, as we require the attributes to be explicitly defined. + +#### Destroy {#destroy} + +Destroy specs drop the payload/instance-building entirely and just find and destroy the record, asserting the count decreases: + +```ruby +describe 'destroying' do + let!(:employee) { create(:employee) } + + let(:instance) do + EmployeeResource.find(id: employee.id) + end + + it 'works' do + expect { + expect(instance.destroy).to eq(true) + }.to change { Employee.count }.by(-1) + end +end +``` + +#### Side Effects {#side-effects} + +```ruby +it 'works' do + # some assertion + email = ActionMailer::Base.deliveries.last + expect(email.subject).to eq('Welcome!') +end +``` + +It's common for write operations to cause side-effects, such as sending an email or updating an audit trail. It's recommended to test these *within the same "it" block* unless the logic gets particularly intense. Though "one expectation per test" works well for unit tests, integration tests can take longer to run and the performance penalty isn't worth it. + +## API Tests {#api-tests} + +There are five test files for each Resource: + +* `spec/api/v1/employees/index_spec.rb` +* `spec/api/v1/employees/show_spec.rb` +* `spec/api/v1/employees/create_spec.rb` +* `spec/api/v1/employees/update_spec.rb` +* `spec/api/v1/employees/destroy_spec.rb` + +### Reads {#api-reads} + +#### #index {#index} + +```ruby +require 'rails_helper' + +RSpec.describe "employees#index", type: :request do + let(:params) { {} } + + subject(:make_request) do + jsonapi_get "/api/v1/employees", params: params + end + + describe 'basic fetch' do + let!(:employee1) { create(:employee) } + let!(:employee2) { create(:employee) } + + it 'works' do + expect(EmployeeResource).to receive(:all).and_call_original + make_request + expect(response.status).to eq(200) + expect(jsonapi_data.map(&:jsonapi_type).uniq) + .to match_array(['employees']) + expect(jsonapi_data.map(&:id)) + .to match_array([employee1.id, employee2.id]) + end + end +end +``` + +#### #show {#show} + +Same shape as `#index`, but requests a single Employee by id and asserts against the singular `d` node instead of an array: + +```ruby +subject(:make_request) do + jsonapi_get "/api/v1/employees/#{employee.id}", params: params +end + +describe 'basic fetch' do + let!(:employee) { create(:employee) } + + it 'works' do + expect(EmployeeResource).to receive(:find).and_call_original + make_request + expect(response.status).to eq(200) + expect(jsonapi_data.jsonapi_type).to eq('employees') + expect(jsonapi_data.id).to eq(employee.id) + end +end +``` + +### Writes {#api-writes} + +#### #create {#api-create} + +```ruby +require 'rails_helper' + +RSpec.describe "employees#create", type: :request do + subject(:make_request) do + jsonapi_post "/api/v1/employees", payload + end + + describe 'basic create' do + let(:payload) do + { + data: { + type: 'employees', + attributes: { + first_name: 'Jane' + } + } + } + end + + it 'works' do + expect(EmployeeResource).to receive(:build).and_call_original + expect { + make_request + }.to change { Employee.count }.by(1) + expect(response.status).to eq(201) + end + end +end +``` + +You probably only want to add attributes required to pass validation, here. We don't assert on attributes of the created record (save this for your Resource test). One easy way to do this is to pass randomized data from your factory: + +```ruby +let(:payload) do + { + data: { + type: 'employees', + attributes: attributes_for(:employee) + } + } +end +``` + +See also: [Dealing with required belongs_to relationships](#required-belongs-to). + +#### #update {#api-update} + +Same as `#create`, but the payload finds an existing employee by `id` and the assertion checks that the record's attributes changed, rather than the count: + +```ruby +subject(:make_request) do + jsonapi_put "/api/v1/employees/#{employee.id}", payload +end + +describe 'basic update' do + let!(:employee) { create(:employee) } + + let(:payload) do + { + data: { + id: employee.id.to_s, + type: 'employees', + attributes: { + first_name: 'changed!' + } + } + } + end + + it 'updates the resource' do + expect(EmployeeResource).to receive(:find).and_call_original + expect { + make_request + }.to change { employee.reload.attributes } + expect(response.status).to eq(200) + end +end +``` + +We don't assert on specific attributes here - save that for your Resource test. Just like `#create`, you may want to use FactoryBot to generate randomized attributes: + +```ruby +let(:payload) do + { + data: { + id: employee.id.to_s, + type: 'employees', + attributes: attributes_for(:employee) + } + } +end +``` + +#### #destroy {#api-destroy} + +```ruby +subject(:make_request) do + jsonapi_delete "/api/v1/employees/#{employee.id}" +end + +describe 'basic destroy' do + let!(:employee) { create(:employee) } + + it 'updates the resource' do + expect(EmployeeResource).to receive(:find).and_call_original + expect { make_request }.to change { Employee.count }.by(-1) + expect { employee.reload } + .to raise_error(ActiveRecord::RecordNotFound) + expect(response.status).to eq(200) + expect(json).to eq('meta' => {}) + end +end +``` + +The response body is asserted to match the [JSONAPI specification for delete responses](http://jsonapi.org/format/#crud-deleting-responses-200): a 200 status with an empty `meta` object. + +## Context {#context} + +Occasionally you'll need to set context for tests. The most common scenario is authorization: + +```ruby +attribute :salary, :integer, readable: :admin? + +def admin? + context.current_user.admin? +end +``` + +When using Rails, `context` is the controller associated to the request. We can manually set context in tests: + +```ruby +let(:user) { double(admin?: true) } +let(:ctx) { double(current_user: user) } + +it 'works' do + Graphiti.with_context ctx do + render + end + expect(jsonapi_data[0].salary).to eq(100_000) +end +``` + +## Schema Validation {#schema-validation} + +Graphiti comes with built-in backwards-compatibility tests. We do this by comparing the current version of the schema with one previously checked-in. + +These tests are added at the bottom of `spec/rails_helper.rb`: + +```ruby +Graphiti::SpecHelpers::RSpec.schema! +``` + +Whenever you run tests, the schema check will *also* run. If we find any backwards-incompatibilities - attributes removed, types changed, default sort direction modified, etc - the schema test will fail with an output detailing all incompatibilities. + +When the schema test succeeds, it will overwrite the existing schema file with the new schema. It will not do this on failure. + +There are times when you want to accept an incompatibility and move on anyway. In this case, use `FORCE_SCHEMA`: + +```bash +$ FORCE_SCHEMA=true bin/rspec +``` + +The same checks run as rake tasks, for a CI step that does not run the suite: + +```bash +$ bin/rake graphiti:schema:check # fails if the file is missing, outdated, or backwards-incompatible +$ bin/rake graphiti:schema:generate # writes it, refusing incompatible changes unless FORCE_SCHEMA=true +``` + +Both use `Graphiti.config.schema_path`. An engine that keeps its own schema file passes a path instead, resolved against the directory the task runs in: + +```bash +$ bin/rake "graphiti:schema:check[spec/support/schema.json]" +``` + +`schema!` takes the same path as a keyword argument, `Graphiti::SpecHelpers::RSpec.schema!(path: "spec/support/schema.json")`. For anything else, `Graphiti::Schema.check` answers `missing?`, `stale?`, `compatible?` and `errors` for a given path without writing. + +## Generators {#generators} + +The [Resource generator](/concepts/resources#generators) will create both Resource and API tests for you. Use these as templates to implement your tests. + +You can also run + +```bash +$ rails generate graphiti:api_test RESOURCE [options] +``` + +For example + +```bash +$ rails generate graphiti:api_test EmployeeResource -a index show +``` + +To generate only the API tests. This can be particularly helpful because API tests are mostly boilerplate that does not need to be manually edited. Pass the `-a` option to limit RESTful actions. + +## Testing Spectrum {#testing-spectrum} + +There's no single right level of test coverage. Teams vary. Our guides favor treating logicless configuration (filters, sorts, sideloads) as covered by Graphiti itself and by schema validation, adding Resource/API tests mainly where there's custom logic - but consider heavier coverage if you're doing a major upgrade or swapping datastores. + +## Double-Testing Units {#double-testing-units} + +A custom filter backed by an ActiveRecord scope can feel like it needs both a model unit test and a near-identical Resource integration test. Use [RSpec shared_context](https://relishapp.com/rspec/rspec-core/docs/example-groups/shared-context) to share the seed data between them, or, if the overhead isn't worth it, mark the scope `# @api private` and skip the unit test until the scope needs to be reused elsewhere. diff --git a/docs/topics/without-activerecord.md b/docs/topics/without-activerecord.md new file mode 100644 index 00000000..4e235bde --- /dev/null +++ b/docs/topics/without-activerecord.md @@ -0,0 +1,324 @@ +--- +title: 'Usage Without ActiveRecord' +--- + +# Usage Without ActiveRecord +Graphiti was built to be used with any ORM or datastore, from PostgreSQL +to elasticsearch to `Net::HTTP`. In fact, Graphiti itself is tested with +Plain Old Ruby Objects (POROs). + +This cookbook will show how to customize a resource around a particular datastore, and how to package those +customizations into a reusable adapter. We'll use an in-memory datastore +and Plain Old Ruby Objects (POROs) here, but the lessons apply to any +datastore. + +For working code, see [this branch of the sample application](https://github.com/graphiti-api/employee_directory/blob/poro/app/resources/post_resource.rb). + +We'll start with this PORO model: + +```ruby +class Post + # Define getters/setters + # e.g. post.title = 'foo' + ATTRS = [:id, :title] + ATTRS.each { |a| attr_accessor(a) } + + # Instantiate with hash of attributes + # e.g. Post.new(title: 'foo') + def initialize(attrs = {}) + attrs.each_pair { |k,v| send(:"#{k}=", v) } + end + + # This part only needed for our particular + # persistence implementation; you may not need it + # e.g. post.attributes # => { title: 'foo' } + def attributes + {}.tap do |attrs| + ATTRS.each do |name| + attrs[name] = send(name) + end + end + end +end +``` + +And this in-memory datastore: + +```ruby +# If we were working with more than just Posts, we'd need a 'type' +# field here as well, to simulate a table name. +DATA = [ + { id: 1, title: 'Graphiti' }, + { id: 2, title: 'is' }, + { id: 3, title: 'super' }, + { id: 4, title: 'dope' } +] +``` + +## Resource Overrides {#resource-overrides} + +If it's your first time with a new ORM or datastore, we recommend +putting the logic in the Resource first. Once things are working *and* +there are multiple uses of the same overrides, package them into an +Adapter. + +```ruby +class PostResource < ApplicationResource + self.adapter = Graphiti::Adapters::Null + + attribute :title, :string + + def base_scope + {} + end + + def resolve(scope) + DATA.map { |d| Post.new(d) } + end +end +``` + +Here we're using the `Null` adapter, which acts as a dumb pass-through. +This can be helpful when you just want to get running for a simple use +case and don't want errors around features you haven't implemented yet. +But it can also be confusing when you expect certain codepaths to +be hit. Mostly just be aware of `Null`'s behavior, or use `Graphiti::Adapters::Abstract` to get helpful errors around what's not +implemented. + +We're also supplying an explicit `base_scope`. This is the beginning +query object we'll modify as params come in. In the case of +ActiveRecord, we might want an `ActiveRecord::Relation` like `Post.all`. For our example, we'll modify a simple ruby hash (keep in +mind the premise of building a hash of options and passing it off to a +client can apply to any datastore). + +Finally, we're [resolving that scope](/concepts/resources#resolve), +returning the full dataset for now. The contract of `#resolve` is to return an array of model instances, hence `DATA.map { |d| Post.new(d) +}`. + +#### Sorting {#sorting} + +```ruby +sort_all do |scope, attribute, direction| + scope[:sort].merge!(attribute: att, direction: dir) +end + +def base_scope + { sort: {} } +end + +def resolve(scope) + if sort = scope[:sort].presence + data = DATA.sort_by { |d| d[sort[:attribute].to_sym] } + data = data.reverse if sort[:direction] == :desc + end + DATA.map { |d| Post.new(d) } +end +``` + +We modified the base scope with a default hash key, `:sort`. When the +user requests sorting, we record this by merging into the hash. We can +then reference that information on the scope when resolving. + +Note the `sort_all` scope block, in fact all scope blocks, must return the scope. + +#### Paginating {#paginating} + +```ruby +paginate do |scope, current_page, per_page| + scope.merge!(current_page: current, per_page: per) +end + +def resolve(scope) + # ... sorting ... + start = (scope[:current_page] - 1) * scope[:per_page] + stop = start + scope[:per_page] + data = data[start...stop] + # ... return models ... +end +``` + +Again: merge into the scope, then reference the scope data when +resolving. + +#### Filtering {#filtering} + +```ruby +filter :title, only: [:eq] do + eq do |scope, value| + scope[:filters][attribute] = value + scope + end +end + +def base_scope(*) + { sort: {}, filters: {} } +end + +def resolve(scope) + # ... sorting ... + scope[:filters].each_pair do |k, v| + data = data.select { |d| d[k.to_sym].in?(v) } + end + # ... pagination ... + # ... return models ... +end +``` + +Same as above examples. Again, we must return the scope object +from the filter function. + +#### Persisting {#persisting} + +All at once: + +```ruby +# Instantiate a model for #create +def build(model_class) + model_class.new +end + +# Used for create/update +def assign_attributes(model, attributes) + attributes.each_pair do |k, v| + model.send(:"#{k}=", v) + end +end + +# Used for create/update +def save(model) + attrs = model.attributes.dup + attrs[:id] ||= DATA.length + 1 + if existing = DATA.find { |d| d[:id].to_s == attrs[:id].to_s } + existing.merge!(attrs) + else + DATA << attrs + end + model +end + +# Used for destroy +def delete(model) + DATA.reject! { |d| d[:id].to_s == model.id.to_s } + model +end +``` + +These are the overrides for persistence operations. You are encouraged +**not** to override `create/update/destroy` directly and instead use +[Persistence Lifecycle Hooks](/concepts/persisting#persistence-lifecycle-hooks). + +## Adapters {#adapters} + +OK so we have all our read and write operations working correctly. But +if we had multiple Resources all using an in-memory datastore, you'd see +this logic repeated all over the place. Let's create an adapter to [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) +up this logic. + +There isn't much more to do than copy/paste what we've already done. +Let's start with our `base_scope`, sorting, and pagination: + +```ruby +class POROAdapter < Graphiti::Adapters::Abstract + def base_scope(*) + { sort: {}, filters: {} } + end + + def paginate(scope, current, per) + scope.merge!(current_page: current, per_page: per) + end + + def order(scope, att, dir) + scope[:sort].merge!(attribute: att, direction: dir) + scope + end + + def resolve(scope) + data = DATA + if sort = scope[:sort].presence + data = data.sort_by { |d| d[sort[:attribute].to_sym] } + data = data.reverse if sort[:direction] == :desc + end + start = (scope[:current_page] - 1) * scope[:per_page] + stop = start + scope[:per_page] + data = data[start...stop] + + data.map { |d| resource.model.new(d) } + end +end +``` + +There's really nothing here we haven't seen before. We're taking the +code we originally wrote, and sticking it into the interface defined by +`Graphiti::Adapters::Abstract`. + +There's a *little* more to do with filtering: + +```ruby +def filter(scope, attribute, value) + scope[:filters][attribute] = value + scope +end +alias :filter_string_eq :filter +alias :filter_integer_eq :filter +alias :filter_date_eq :filter +# ... etc ... +``` + +The logic is the same, but we have a separate method for each filter +operator. This allows us to query differently based on the type - for +instance, ActiveRecord will default to case-insensitive for strings, but +straight equality for integers. If you don't need operator-specific +logic, just `alias` as you see here. + +You may want to limit the default operators we expect to work with a +given type. Let's say your backend allows straight equality for strings, +but doesn't support `prefix`, `suffix`, etc. You can specify this in +your adapter: + +```ruby +def self.default_operators + super.tap do |built_in| + built_in[:string] = [:eq] + end +end + +# or avoid super altogether + +def self.default_operators + { + string: [:eq], + integer: [:eq] + # ... etc ... + } +end +``` + +**That's it for reads**. For writes, I'll post the entire adapter code +below - again, it's just copy/pasting what we already wrote into a +slightly different format. + +```ruby +def destroy(model) + Post::DATA.reject! { |d| d[:id].to_s == model.id.to_s } + model +end + +def save(model) + attrs = model.attributes.dup + attrs[:id] ||= Post::DATA.length + 1 + if existing = Post::DATA.find { |d| d[:id].to_s == attrs[:id].to_s } + existing.merge!(attrs) + else + Post::DATA << attrs + end + model +end + +# For wrapping persistence operations in a DB transactions +# Our in-memory DB doesn't have transactions, so just yield +def transaction(*) + yield +end +``` + +That's really it. [See the working code in Employee Directory here](https://github.com/graphiti-api/employee_directory/blob/poro/app/resources/post_resource.rb). diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md new file mode 100644 index 00000000..04ee1962 --- /dev/null +++ b/docs/tutorial/index.md @@ -0,0 +1,58 @@ +--- +title: 'Tutorial' +--- + +

+ +

+ +# Tutorial +This tutorial serves as a deeper-dive into Graphiti development, +building an Employee Directory application. We purposefully built this +to illustrate common - but non-trivial - scenarios present in many +applications. + +You'll need Ruby 3.2+ and Rails 7.1+ installed. Step 0 starts from an empty directory, so nothing else is assumed. + +A core concept of Graphiti is **Test-First** - the most pleasant way to +develop Graphiti is by starting with an [integration test](/topics/testing). But that can add a lot of noise to a tutorial like this. Though we'll occasionally touch on testing - and the git diffs at the top of each section contain the necessary tests - we won't test first for the purposes of this tutorial. + + +### Server Side: Rails + +[Rails Sample Application](https://github.com/graphiti-api/employee_directory) + +* [Step 0: Bootstrapping](/tutorial/step_0) +* [Step 1: Initial Resource](/tutorial/step_1) +* [Step 2: Has Many](/tutorial/step_2) +* [Step 3: Belongs To](/tutorial/step_3) +* [Step 4: Customizing Queries](/tutorial/step_4) +* [Step 5: Has One](/tutorial/step_5) +* [Step 6: Customizing Writes](/tutorial/step_6) +* [Step 7: Many-to-Many](/tutorial/step_7) +* [Step 8: Polymorphic +Relationships](/tutorial/step_8) +* [Step 9: Polymorphic Resources](/tutorial/step_9) + + + +### Client Side: VueJS (diff-only) + +[VueJS Sample Application](https://github.com/graphiti-api/employee-directory-vue) + + +* [Step 0: Setup](https://github.com/graphiti-api/employee-directory-vue/commit/be690c3038380e17e326935d595a0b83fc8004f9) + * Run after `vue create employee-directory-vue` using [Vue CLI](https://cli.vuejs.org). +* [Step 1: Define Models](https://github.com/graphiti-api/employee-directory-vue/compare/step_0_setup...step_1_models) +* [Step 2: Data Grid](https://github.com/graphiti-api/employee-directory-vue/compare/step_1_models...step_2_data_grid) +* [Step 3: Relationships](https://github.com/graphiti-api/employee-directory-vue/compare/step_2_data_grid...step_3_includes) +* [Step 4: Filtering](https://github.com/graphiti-api/employee-directory-vue/compare/step_3_includes...step_4_filtering) +* [Step 5: Sorting](https://github.com/graphiti-api/employee-directory-vue/compare/step_4_filtering...step_5_sorting) +* [Step 6: Total Count](https://github.com/graphiti-api/employee-directory-vue/compare/step_5_sorting...step_6_stats) +* [Step 7: Pagination](https://github.com/graphiti-api/employee-directory-vue/compare/step_6_stats...step_7_pagination) +* [Step 8: Basic Form Setup](https://github.com/graphiti-api/employee-directory-vue/compare/step_7_pagination...step_8_basic_form_setup) +* [Step 9: Dropdown](https://github.com/graphiti-api/employee-directory-vue/compare/step_8_basic_form_setup...step_9_dropdown) +* [Step 10: Nested Form Submission](https://github.com/graphiti-api/employee-directory-vue/compare/step_9_dropdown...step_10_nested_create) +* [Step 11: Validation Errors](https://github.com/graphiti-api/employee-directory-vue/compare/step_10_nested_create...step_11_validations) +* [Step 12: Nested Destroy](https://github.com/graphiti-api/employee-directory-vue/compare/step_11_validations...step_12_nested_destroy) +* [Step 13: Vue-Specific Glue Code](https://github.com/graphiti-api/employee-directory-vue/compare/step_12_nested_destroy...step_13_vue) diff --git a/docs/tutorial/step_0.md b/docs/tutorial/step_0.md new file mode 100644 index 00000000..82295212 --- /dev/null +++ b/docs/tutorial/step_0.md @@ -0,0 +1,93 @@ +--- +title: 'Step 0' +--- + +## Step 0: Bootstrapping + +> [View the Code](https://github.com/graphiti-api/employee_directory/commit/e2552ce212c68b41a3eb8161deb822fff3e159d6) + +Let's start by creating a new Rails project. For help with an existing +project, check out [Installation: From +Scratch](/getting-started/installation). + +We'll use the `-m` option to install from a template, which will add a few gems and apply some setup boilerplate. Accept all the default options. + +```bash +$ rails new employee_directory --api -m https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb +$ cd employee_directory +``` + +> Note: if a network issue prevents you from pointing to this URL directly, you can download the file and and run this command as `-m /path/to/template` + +Feel free to run `git diff` to see what the generator did, otherwise commit the result. You can now head to [Step 1: Basic Resource](/tutorial/step_1), or continue reading to better understand the code. + +#### Digging Deeper 🧐 + +You'll see some boilerplate in `config/routes.rb`: + +```ruby +scope path: "/api/v1", defaults: {format: :jsonapi} do + # your routes go here +end +``` + +This tells Rails that our API routes will be be prefixed - `/api/v1` by default. It also says that if no extension is in the URL (`.json`, `.xml`, etc), default +to the [JSONAPI Specification](http://jsonapi.org). + +Let's look at the above `ApplicationResource`: + +```ruby +class ApplicationResource < Graphiti::Resource + self.abstract_class = true + + # We'll be using ActiveRecord + self.adapter = Graphiti::Adapters::ActiveRecord + + # Links are generated from base_url + endpoint_namespace + self.base_url = ENV.fetch('BASE_URL', 'http://localhost:3000') + self.endpoint_namespace = '/api/v1' +end +``` + +This should be pretty self-explanatory except for + +```ruby +self.base_url = ENV.fetch('BASE_URL', 'http://localhost:3000') +``` + +When deriving and validating [Links](/concepts/links), we'll use the `BASE_URL` variable if +present, falling back to the Rails development default of +`http://localhost:3000`. Unlike a Rails URL helper this needs the scheme and port, because it is the whole prefix every link is built on. This means our Links will look like: + +```ruby +"#{ENV['BASE_URL']}/#{Resource.endpoint_namespace}/#{Resource.type}" +``` + +For example: + +```ruby +http://my-website.com/api/v1/employees +``` + +Read more in the [Links Guide](/concepts/links). + +Finally, there's some boilerplate in `ApplicationController`: + +```ruby +class ApplicationController < ActionController::API + include Graphiti::Rails::Controller +end +``` + +This wires Graphiti into the request cycle: the Graphiti context, the debugger, JSON:API error rendering, and `respond_to` (which `ActionController::API` normally strips out). Controllers render with `render jsonapi:`, and to render simple nested JSON like default Rails, we'll only need to add `.json` to the URL. (Prefer writing `respond_with(posts)`? The optional [responders integration](/getting-started/installation#responders) is one gem and one include away.) + +That's it for basic setup! + + +

+ + NEXT - + Step 1: Basic Resource + » + +

diff --git a/docs/tutorial/step_1.md b/docs/tutorial/step_1.md new file mode 100644 index 00000000..8623e226 --- /dev/null +++ b/docs/tutorial/step_1.md @@ -0,0 +1,199 @@ +--- +title: 'Step 1' +--- + +## Step 1: Basic Resource + +> [View the Diff](https://github.com/graphiti-api/employee_directory/commit/45c1c92e14fb1c3a47b8ed246ceb2cba50e97c72) + +We'll be working with a single database table, `employees`: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
idfirst_namelast_nameagecreated_atupdated_at
1HomerSimpson392018-09-042018-09-04
2WaylonSmithers652018-09-042018-09-04
3MontyBurns1232018-09-042018-09-04
+ +### The Rails Stuff 🚂 + +Use the built-in generator to create the database table +and corresponding `ActiveRecord` model: + +```bash +$ bin/rails g model Employee first_name:string last_name:string age:integer +$ bin/rails db:migrate +``` + +Now let's seed some random development data, using [Faker](https://github.com/stympy/faker) (which was installed in [Step 0](/tutorial/step_0)): + +```ruby +# db/seeds.rb +Employee.delete_all # Ensure the DB is cleaned each run + +100.times do + Employee.create! first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + age: rand(20..80) +end +``` + +Run this seed file with + +```bash +$ bin/rails db:seed +``` + +### The Graphiti Stuff 🎨 + +Just like Rails, Graphiti has built-in generators. Let's generate +the corresponding Resource for our `Employee` model: + +```bash +$ bin/rails g graphiti:resource Employee first_name:string last_name:string age:integer created_at:datetime updated_at:datetime +``` + +This generated a few things, but for now let's focus on +`EmployeeResource`: + +```ruby +class EmployeeResource < ApplicationResource + attribute :first_name, :string + attribute :last_name, :string + attribute :age, :integer + attribute :created_at, :datetime, writable: false + attribute :updated_at, :datetime, writable: false +end +``` + +This code defined the [RESTful Resource](https://restful-api-design.readthedocs.io/en/latest/resources.html) we want our API to expose. Let's run our server and see what it does: + +```bash +$ bin/rails s +``` + +Visit `localhost:3000/api/v1/employees`. You should see a [JSONAPI Response](http://jsonapi.org): + +
+ +![jsonapi](/assets/img/legacy/legacy-0378a3bb39.png) + +
+ +If you find the payload a little intimidating, add `.json` to the URL for a more traditional response, or `.xml` for XML. Both are different **renderings** of the same `EmployeeResource`. + +`Resources` are comprised of `Attribute`s: + +```ruby +# app/resources/employee_resource.rb +attribute :first_name, :string +``` + +Each attribute defines behavior for: + +* Reading (display) +* Writing +* Sorting +* Filtering +* Fieldsets + +Let's start with simple display, turning `first_name` into all capital +letters: + +```ruby +# app/resources/employee_resource.rb +attribute :first_name, :string do + # @object is your model instance + @object.first_name.upcase +end +``` + +This is the most important thing to understand about Resources: they are just a collection of defaults, all of which can be overridden. `attribute :first_name` is shorthand for `attribute :first_name do @object.first_name end`. + +We'll go into further Resource customizations over the course of this tutorial. For now, undo the capitalization change above, and verify our out-of-the-box defaults: the same filter, sort, and pagination capabilities you exercised in the [Quickstart](/getting-started/first-api#querying) work here too, just against `employees` instead of `posts`. See the [Overview guide](/concepts/overview) for the full capability reference. + +Write operations are easiest to verify with integration tests, which were created when we generated our Resource: an **API Spec** covering the request/response cycle, and a **Resource Spec** covering the Resource's logic directly. See the [Testing Guide](/topics/testing) for what these look like and how they differ. The example there uses the same `create` payload shape the generator produced for `EmployeeResource`. + +Before we run these specs, we need to edit our [factories](https://github.com/thoughtbot/factory_bot) to ensure +dynamic, randomized data. Let's change this: + +```ruby +# spec/factories/employee.rb + +FactoryBot.define do + factory :employee do + first_name { "MyString" } + last_name { "MyString" } + age { 1 } + end +end +``` + +To + +```ruby +# spec/factories/employee.rb + +FactoryBot.define do + factory :employee do + first_name { Faker::Name.first_name } + last_name { Faker::Name.last_name } + age { rand(20..80) } + end +end +``` + +Now run the generated specs: + +```bash +$ bundle exec rspec +``` + +You'll see 11 tests pass, with 3 pending. One of the pending specs was +autogenerated by rails - you can delete `spec/models/employee_spec.rb` +for now. + +That leaves us with two "update" specs. These are marked pending so you +can manage the data yourself. Follow the comments in these specs to add +attributes and get them passing. + + +

+ + NEXT - + Step 2: Has Many + » + +

diff --git a/docs/tutorial/step_2.md b/docs/tutorial/step_2.md new file mode 100644 index 00000000..0ffe6476 --- /dev/null +++ b/docs/tutorial/step_2.md @@ -0,0 +1,312 @@ +--- +title: 'Step 2' +--- + +## Step 2: Has Many + +> [View the Code](https://github.com/graphiti-api/employee_directory/compare/step_1_employees...step_2_positions) + +We'll be adding the database table `positions`: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
idemployee_idtitleactivehistorical_indexcreated_atupdated_at
1900Engineertrue12018-09-042018-09-04
2900Interntrue22018-09-042018-09-04
3800Managertrue12018-09-042018-09-04
+ +Because this table tracks all historical positions, we have the +`historical_index` column. This tells the order the employee moved through each position, where `1` is most recent. + +### The Rails Stuff 🚂 + +Generate the `Position` model: + +```bash +$ bin/rails g model Position title:string active:boolean historical_index:integer employee:belongs_to +$ bin/rails db:migrate +``` + +Update the `Employee` model with the association, too: + +```ruby +# app/models/employee.rb +has_many :positions +``` + +And update our seed data: + +```ruby +# db/seeds.rb +[Employee, Position].each(&:delete_all) + +100.times do + employee = Employee.create! first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + age: rand(20..80) + + (1..2).each do |i| + employee.positions.create! title: Faker::Job.title, + historical_index: i, + active: i == 1 + end +end +``` + +```bash +$ bin/rails db:seed +``` + +### The Graphiti Stuff 🎨 + +Let's start by running the same command as before to create +`PositionResource`: + +```bash +$ bin/rails g graphiti:resource Position title:string active:boolean +``` + +We'll need to add the association, just like ActiveRecord: + +```ruby +# app/resources/employee_resource.rb +has_many :positions +``` + +...and a corresponding filter: + +```ruby +# app/resources/position_resource.rb +filter :employee_id, :integer +``` + +If you visit `/api/v1/employees`, you'll see a number of HTTP +[Links](https://graphiti.dev/guides/concepts/links) +that allow lazy-loading positions. Or, if you visit +`/api/v1/employees?include=positions`, you'll load the employees and +positions in a single request. We'll dig a bit deeper into this logic +in the section below. + +Before we get there, let's revisit the `historical_index` column. For now, let's +treat this as an implementation detail that the API should not expose - +let's say we want to support sorting on this attribute but nothing else: + +```ruby +attribute :historical_index, :integer, only: [:sortable] +``` + +We're almost done, but if you run your tests you'll see two outstanding +errors. This is because Rails requires `belongs_to` associations by default. We can't save a `Position` without its corresponding `Employee`. + +We can solve this in three ways: + +* Turn this off globally, with [config.active_record.belongs_to_required_by_default](https://edgeguides.rubyonrails.org/configuring.html#configuring-active-record). You may want to do this in test-mode only. +* Turn this off for the specific association: `belongs_to :employee, optional: true`. +* Associate an `Employee` as part of the API request. + +We'll take for the last option. Look at +`spec/resources/position/writes_spec.rb`: + +```ruby +RSpec.describe PositionResource, type: :resource do + describe 'creating' do + let(:payload) do + { + data: { + type: 'positions', + attributes: { } + } + } + end + + let(:instance) do + PositionResource.build(payload) + end + + it 'works' do + expect { + expect(instance.save).to eq(true) + }.to change { Position.count }.by(1) + end + end +end +``` + +When running our tests, let's make sure the `historical_index` column +reflects the order we created the positions. This code recalculates +everything after a record is saved: + +```ruby +# spec/factories/position.rb +FactoryBot.define do + factory :position do + employee + + title { Faker::Job.title } + + after(:create) do |position| + unless position.historical_index + scope = Position + .where(employee_id: position.employee.id) + .order(created_at: :desc) + scope.each_with_index do |p, index| + p.update_attribute(:historical_index, index + 1) + end + end + end + end +end +``` + +Let's associate an `Employee`. Start by seeding the data: + +```ruby +let!(:employee) { create(:employee) } +``` + +And associate via `relationships`: + +```ruby +let(:payload) do + { + data: { + type: 'positions', + attributes: { }, + relationships: { + employee: { + data: { + id: employee.id.to_s, + type: 'employees' + } + } + } + } + } +end +``` + +To ensure the `PositionResource` will process this relationship, the +last step is to add it: + +```ruby +# app/resources/position_resource.rb +belongs_to :employee +``` + +This will associate the `Position` to the `Employee` as part of the +creation process. The test should now pass - make the same change to +`spec/api/v1/positions/create_spec.rb` to get a fully-passing test +suite. + +#### Digging Deeper 🧐 + +Why did we need the `employee_id` filter above? To explain that, let's dive deeper into the logic connecting Resources. + +If you hit `/api/v1/employees`, you'll see a number of +[Links](https://graphiti.dev/guides/concepts/links) in the +response. These are useful for lazy-loading, but the same logic +applies to eager loading. Let's take a look at a Link to see how these +Resources connect together: + +```ruby +{ + ... + relationships: { + positions: { + links: { + related: "http://localhost:3000/api/v1/positions?filter[employee_id]=1" + } + } + } + ... +} +``` + +The salient bit: `/positions?filter[employee_id]=1`. In other words, +fetch all Positions for the given Employee id.That means, whether we're lazy-loading data in separate requests or +eager-loading in a single request, **the same logic fires +under-the-hood**: + +```ruby +PositionResource.all({ + filter: { employee_id: 1 } +}) +``` + +This means we need `filter :employee_id, :integer` to satisfy the query. + +We can customize the logic connecting Resources in a few different +ways. First some simple options: + +```ruby +has_many :positions, foreign_key: :emp_id, primary_key: :eid +``` + +So far so good. The logic, and corresponding Link, both update as you'd +expect (though we'd of course need a corresponding `filter :emp_id, :integer` on `PositionResource`). + +Those options are just simple versions of parameter customization. +You can customize parameters connecting Resources with the `params` block: + +```ruby +has_many :positions do + params do |hash, employees| + hash[:filter] # => { employee_id: employees.map(&:id) } + hash[:filter][:active] = true + hash[:sort] = '-created_at' + end +end +``` + +Customizing these params affects the Link as well as the eager-load +logic. Remember the parameters here should reflect the JSON:API +specification, or anything `PositionResource.all` accepts. + +These are the most common options, but there's a bunch more. Check +out the [Resource Relationships Guide](/concepts/relationships) to dig even deeper. + + + +

+ + NEXT - + Step 3: Belongs To + » + +

diff --git a/docs/tutorial/step_3.md b/docs/tutorial/step_3.md new file mode 100644 index 00000000..8faa5ee6 --- /dev/null +++ b/docs/tutorial/step_3.md @@ -0,0 +1,142 @@ +--- +title: 'Step 3' +--- + +### Step 3: Belongs To + +> [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_2_positions...step_3_departments) + +We'll be adding the database table `departments`: + + + + + + + + + + + + + + + + + + + + + + +
idname
1Engineering
2Safety
3QA
+ +We'll also be adding a `department_id:integer` foreign key column to the `positions` table. + +### The Rails Stuff 🚂 + +Generate the `Department` model: + +```bash +$ bin/rails g model Department name:string +``` + +To add the foreign key to `positions`: + +```bash +$ bin/rails g migration add_department_id_to_positions +``` + +```ruby +class AddDepartmentIdToPositions < ActiveRecord::Migration[7.1] + def change + add_foreign_key :positions, :departments + end +end +``` + +Update the database: + +```bash +$ bin/rails db:migrate +``` + +Update our seed file: + +```ruby +[Employee, Position, Department].each(&:delete_all) + +engineering = Department.create! name: 'Engineering' +safety = Department.create! name: 'Safety' +qa = Department.create! name: 'QA' +departments = [engineering, safety, qa] + +100.times do + employee = Employee.create! first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + age: rand(20..80) + + (1..2).each do |i| + employee.positions.create! title: Faker::Job.title, + historical_index: i, + active: i == 1, + department: departments.sample + end +end +``` + +Make sure to update `spec/factories/departments.rb` with randomized +data. Then, since this is also a required relationship, update +`spec/factories/positions.rb` to always seed a department when we ask to +create a position: + +```ruby +factory :position do + employee + department + + # ... code ... +end +``` + +### The Graphiti Stuff 🎨 + +You should be used to this by now: + +```bash +bin/rails g graphiti:resource Department name:string +``` + +Add the association: + +```ruby +# app/resources/position_resource.rb +belongs_to :department +``` + +And review the end of [Step 2](/tutorial/step_2) to get all your specs +passing (add the department to the request payload). Practice makes perfect! + +#### Digging Deeper 🧐 + +We didn't need a filter like we did in step two. That's +because the primary key connecting the Resources is `id` by +default. In other words, the Link would be something like: + +```bash +/departments?filter[id]=1 +``` + +Which we get out-of-the-📦 + +But remember, you can customize these relationships just like the +previous `has_many` section. + + +

+ + NEXT - + Step 4: Customizing Queries + » + +

diff --git a/docs/tutorial/step_4.md b/docs/tutorial/step_4.md new file mode 100644 index 00000000..4bd930f2 --- /dev/null +++ b/docs/tutorial/step_4.md @@ -0,0 +1,135 @@ +--- +title: 'Step 4' +--- + +### Step 4: Customizing Queries + +> [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_3_departments...step_4_customizations) + +So far, we've done fairly straightforward queries. If a user filters on +`first_name`: + +`/api/v1/employees?filter[first_name]=Foo` + +We'll query the equivalent database column: + +```ruby +Employee.where(first_name: 'Foo') +``` + +But what if there's more complex logic? Let's say we want to sort +Employees on their `title` - which comes from the `positions` table. +How would that work? + +### The Rails Stuff 🚂 + +First, we need to get data for an Employee's **current** position. +Let's start by defining what `current` means + +```ruby +# app/models/position.rb +scope :current, -> { where(historical_index: 1) } +``` + +> See the [ActiveRecord Scopes](https://guides.rubyonrails.org/active_record_querying.html#scopes) documentation if you're unfamiliar with this concept. + +Reference this scope in a new association: + +```ruby +has_one :current_position, + -> { current }, + class_name: 'Position' +``` + +Before moving on, let's review what we need to do. The `ActiveRecord` +code for sorting Employees on their current position's title would be: + +```ruby +Employee.joins(:current_position).merge(Position.order(title: :asc)) +``` + +Let's wire this up to Graphiti: + +### The Graphiti Stuff 🎨 + +We're only going to **sort** and **filter** on the `title` attribute - +never display or persist. So start by defining the attribute as such: + +```ruby +attribute :title, :string, only: [:filterable, :sortable] +``` + +Then the `sort` DSL to place our custom query: + +```ruby +# app/resources/employee_resource.rb +sort :title do |scope, direction| + scope.joins(:current_position).merge(Position.order(title: direction)) +end +``` + +That's it! When a request to sort on the title comes in, we'll alter our +scope to join on the `positions` table, and order based on the current position `title`. + +The solution for filtering is similar: + +```ruby +# app/resources/employee_resource.rb +filter :title do + eq do |scope, value| + scope.joins(:current_position).merge(Position.where(title: value)) + end +end +``` + +We can now filter on title: + +`/api/v1/employees?filter[title]=Foo` + +Let's do one more example - how would we order Employees by department +name? We *could* start the same way: + +```ruby +attribute :department_name, :string, only: [:sortable] +``` + +But if we're ***only*** sorting, this is actually redundant. Whenever we +use the `sort` or `filter` DSL, we're creating a sort-only or +filter-only attribute under the hood. So let's define everything in one +shot: + +```ruby +sort :department_name, :string do |scope, value| + scope.joins(current_position: :department) + .merge(Department.order(name: value)) +end +``` + +Remember: you only need to pass the type as the second argument when an +attribute doesn't already exist. And if you ever get an error saying +something is unfilterable or unsortable, check to see if you've already +defined a filter-only or sort-only attribute using these methods. + +#### Digging Deeper 🧐 + +There's a critical part of Graphiti that makes everything easier: start +by imagining it doesn't exist. + +In other words, the meat of the logic above had nothing to do with +Graphiti code - we're "wiring up" independent ActiveRecord +queries. If you're ever confused about query logic, get things working +without Graphiti first. + +We could have changed the above to ActiveRecord scopes like +`.order_by_title(title)`, making the wiring code even simpler. Consider +doing this when the logic is reusable or particlar complex, but be aware +of the tradeoffs of [double-testing units](https://graphiti.dev/guides/concepts/testing#double-testing-units). + + +

+ + NEXT - + Step 5: Has One + » + +

diff --git a/docs/tutorial/step_5.md b/docs/tutorial/step_5.md new file mode 100644 index 00000000..55da867e --- /dev/null +++ b/docs/tutorial/step_5.md @@ -0,0 +1,69 @@ +--- +title: 'Step 5' +--- + +### Step 5: Has One + +> [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_4_customizations...step_5_has_one) + +In the last step, we introduced the concept of a "current position" to +the model layer. Let's now expose that relationship to the API. + +### The Rails Stuff 🚂 + +We already defined a `Position.current` scope that we'll re-use - +let's just make a small tweak to support the opposite use case as +well: + +```ruby +scope :current, ->(bool) { + clause = { historical_index: 1 } + bool ? where(clause) : where.not(clause) +} +``` + +### The Graphiti Stuff 🎨 + +You might already have an idea how this might work from the prior step +- we'll use the `params` block to customize the relationship. The `has_one` macro ensures the result is treated as a single object and +not an array. + +```ruby +# app/resources/employee_resource.rb +has_one :current_position, resource: PositionResource do + params do |hash| + hash[:filter][:current] = true + end +end +``` + +Which means we'll have to implement that filter - re-using the +ActiveRecord scope we already defined! + +```ruby +filter :current, :boolean do + eq { |scope, value| scope.current(value) } +end +``` + +#### Digging Deeper 🧐 + +In this example, we're able to return only a single record because we +have a `historical_index` column. If this column didn't exist - maybe we're just ordering on `created_at` and taking the first record - we'd +have a problem. What if we were loading 20 employees and wanted the +`current_position` of each - what SQL would limit the resultset +correctly? + +We call this a [faux has_one](/concepts/relationships#faux-has-one) and there's nothing easily done here. Graphiti will ensure only one record +is returned by the API, but the query will take longer and loading extra +records will eat memory. If there are lots of records in the +association, look into adding a column like `historical_index`. + + +

+ + NEXT - + Step 6: Customizing Writes + » + +

diff --git a/docs/tutorial/step_6.md b/docs/tutorial/step_6.md new file mode 100644 index 00000000..62e29c6c --- /dev/null +++ b/docs/tutorial/step_6.md @@ -0,0 +1,82 @@ +--- +title: 'Step 6' +--- + +### Step 6: Customizing Writes + +> [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_5_has_one...step_6_write_customization) + +When we ran the generators (and created a blank Resource class), we got the ability to create, update, and destroy resources for free. You can turn off this behavior with `self.read_only = true`. Or for relationships: `has_many :positions, writable: false`. + +But in RESTful APIs, it's super common for persistence operations to +have side effects - that's how we avoid extraneous verbs and +inconsistent patterns. + +In a prior step, we updated `position` Factory to automatically reorder the `historical_index`: when a new record comes in, all the prior +values need to change. This step will show how to add that behavior to +our API, using hooks that work for a variety of scenarios: sending +emails, checking authorization roles, queuing delayed jobs, and more. + +### The Rails Stuff 🚂 + +Previously, we put the logic that re-ordered the `historical_index` column in the `position` factory. Let's move that to the model so our +tests and API can share the same logic: + +```ruby +# app/models/position.rb +def self.reorder!(employee_id) + scope = Position.where(employee_id: employee_id).order(created_at: :desc) + scope.each_with_index do |p, index| + p.update_attribute(:historical_index, index + 1) + end +end +``` + +```ruby +# spec/factories/positions.rb +# ... code ... +after(:create) do |position| + unless position.historical_index + Position.reorder!(position.employee.id) + end +end +``` + +### The Graphiti Stuff 🎨 + +All Graphiti updates happen within a transaction. We want to insert our +code right before that transaction closes - after the graph of objects +has been persisted and validations have passed. To do that, we'll use +the `before_commit` hook: + +```ruby +before_commit only: [:create, :destroy] do |position| + Position.reorder!(position.employee_id) +end +``` + +Again, the `Position.reorder!` code existed independent of our +API, and was re-used in our factory. + +#### Digging Deeper 🧐 + +Resources come with [Lifecycle Hooks](https://graphiti.dev/guides/concepts/persisting#persistence-lifecycle-hooks), similar to ActiveRecord [Callbacks](https://guides.rubyonrails.org/active_record_callbacks.html). + +Those callbacks have gotten a bad reputation. This is because your Model +can be - is supposed to be - used in a variety of contexts across your +application. Some of those contexts will want a given callback to fire, +others will not, and accomodating the conditionals gets hairy. This is +why many developers move that functionality into [Service Objects](https://engineering.gusto.com/the-rails-callbacks-best-practices-used-at-gusto/). + +But Resource callbacks don't have the same problem - they only fire in +the context of your API, and can be associated to a single endpoint. You +can still use Service Objects if you'd like. Graphiti callbacks wire them up. + + +

+ + NEXT - + Step 7: Many to Many + » + +

diff --git a/docs/tutorial/step_7.md b/docs/tutorial/step_7.md new file mode 100644 index 00000000..15cc8f2e --- /dev/null +++ b/docs/tutorial/step_7.md @@ -0,0 +1,205 @@ +--- +title: 'Step 7' +--- + +### Step 7: Many to Many + +> [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_6_write_customization...step_7_many_to_many) + +Let's add a `Team` relationship: a `Team` can have many `Employee`s, an `Employee` can have many `Team`s. Let's also say a `Team` belongs to a `Department`. + + + + + + + + + + + + + + + + + + + + + + + + + + +
iddepartment_idname
11The A Team
21The B Team
32The C Team
+ +To satisfy this many-to-many use case, we'll need a join model, +`TeamMembership`: + + + + + + + + + + + + + + + + + + + + + + + + + + +
idteam_idemployee_id
111
221
332
+ +### The Rails Stuff 🚂 + +```bash +$ bin/rails g model Team name:string department:belongs_to +$ bin/rails g model TeamMembership employee:belongs_to team:belongs_to +$ bin/rails db:migrate +``` + +Graphiti supports `has_many :through`: + +```ruby +# app/models/employee.rb +has_many :team_memberships +has_many :teams, through: :team_memberships +``` + +```ruby +# app/models/department.rb +has_many :teams +``` + +```ruby +class Team < ApplicationRecord + belongs_to :department + has_many :team_memberships + has_many :employees, through: :team_memberships +end +``` + +```ruby +class TeamMembership < ApplicationRecord + belongs_to :team + belongs_to :employee +end +``` + +Finally, we'll need a new seed file to handle these new associations: + +```ruby +[ + Employee, + Position, + Department, + TeamMembership, + Team +].each(&:delete_all) + +departments = [] +def create_department(name) + dept = Department.create! name: name + dept.teams.create!(name: 'Engineering Team B') + dept.teams.create!(name: 'Engineering Team C') + dept +end + +departments << create_department('Engineering') +departments << create_department('Safety') +departments << create_department('QA') + +100.times do + employee = Employee.create! first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + age: rand(20..80) + + (1..2).each do |i| + employee.positions.create! title: Faker::Job.title, + historical_index: i, + active: i == 1, + department: departments.sample + end + + employee.teams << employee.positions[0].department.teams.sample +end +``` + +### The Graphiti Stuff 🎨 + +```bash +$ bin/rails g graphiti:resource Team name:string +``` + +Let's flesh out our `TeamResource`: + +```ruby +# app/resources/team_resource.rb +class TeamResource < ApplicationResource + attribute :department_id, :integer, only: [:filterable] + attribute :name, :string + + belongs_to :department + many_to_many :employees +end +``` + +The trick here is the `many_to_many` relationship. Let's add the reverse +as well: + +```ruby +# app/resources/employee_resource.rb +many_to_many :teams +``` + +And for good measure: + +```ruby +# app/resources/department_resource.rb +has_many :teams +``` + +We can now get all the usual functionality: fetch Employees and their +Teams in a single request (or vice versa). + +#### Digging Deeper 🧐 + +The `many_to_many` relationship is the only one where Graphiti modifies a separate Resource "under the hood". When we said `many_to_many +:employees`, the `EmployeeResource` got a `team_id` filter, and `many_to_many :teams` created an `employee_id` filter on `TeamResource`. + +This is because the logic is more complex than the default use case. We +don't have a simple `WHERE` clause. We need to join tables and look at +the appropriate primary/foreign keys. If the name of your API +association doesn't match the name of your ActiveRecord association, try +`has_many :things, as: :my_activerecord_relationship` to make the +introspection work correctly - or, write your own filter. + +Sometimes you'll have multiple levels of `has_many :through`. In this case, a simple `many_to_many` isn't enough - check out the [Hopping +Relationships](/topics/hopping-relationships) recipe. + +Think hard before reaching for `many_to_many`. Imagine one Team is the "primary" Team for an Employee. We'd add a `primary` boolean column to the `team_memberships` table...but that table isn't exposed to the API! +Consider if there's a hidden domain concept there. + + +

+ + NEXT - + Step 8: Polymorphic Relationships + » + +

diff --git a/docs/tutorial/step_8.md b/docs/tutorial/step_8.md new file mode 100644 index 00000000..03a1f462 --- /dev/null +++ b/docs/tutorial/step_8.md @@ -0,0 +1,128 @@ +--- +title: 'Step 8' +--- + +### Step 8: Polymorphic Relationships + +> [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_7_many_to_many...step_8_polymorphic_belongs_to) + +Let's introduce the concept of a Note. A Note can belong to a +Department, an Employee, or a Team. For this, we'll need to introduce +the concept of [polymorphism](https://guides.rubyonrails.org/association_basics.html#polymorphic-associations). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
idnotable_idnotable_typebody
11EmployeeA Sample Note!
21DepartmentAnother Sample Note!
31TeamA Third Sample Note!
+ +### The Rails Stuff 🚂 + +```bash +$ rails generate model Note notable:references{polymorphic}:index +$ bin/rails db:migrate +``` + +Make sure to add the corresponding model relationships: + +```ruby +# app/models/employee.rb +has_many :notes, as: :notable +# app/models/team.rb +has_many :notes, as: :notable +# app/models/department.rb +has_many :notes, as: :notable + +# app/models/note.rb +belongs_to :notable, polymorphic: true +``` + +Finally, make sure to edit your seed file - check out the [diff](https://github.com/graphiti-api/employee_directory/compare/step_7_many_to_many...step_8_polymorphic_belongs_to) to see the necessary adjustments. + +### The Graphiti Stuff 🎨 + +```bash +$ bin/rails g graphiti:resource Note body:string +``` + +Let's create our `NoteResource`: + +```ruby +class NoteResource < ApplicationResource + attribute :body, :string + + filter :notable_id, :integer + filter :notable_type, :string, allow: %w(Employee Department Team) + + polymorphic_belongs_to :notable do + group_by(:notable_type) do + on(:Employee) + on(:Team) + on(:Department) + end + end +end +``` + +And corresponding associations: + +```ruby +# app/resources/employee_resource.rb +polymorphic_has_many :notes, as: :notable +# app/resources/team_resource.rb +polymorphic_has_many :notes, as: :notable +# app/resources/department_resource.rb +polymorphic_has_many :notes, as: :notable +``` + +#### Digging Deeper 🧐 + +When defining a polymorphic relationship for our API, we're saying "grab +all the parent records, group them by a `type` column, and execute different queries for each type". This way records with `notable_type == +'Employee'` can hit the `employees` table, but records with `notable_type == 'Department'` could in theory load from a different API +altogether. + +Each of the `on` lines defines a new `belongs_to` association. That +means you can customize just like always: + +```ruby +on(:Team).belongs_to :team, resource: SomeCustomTeamResource do + # assign {} + # link {} + # ... etc ... +end +``` + + +

+ + NEXT - + Step 9: Polymorphic Resources + » + +

diff --git a/docs/tutorial/step_9.md b/docs/tutorial/step_9.md new file mode 100644 index 00000000..b1a9b114 --- /dev/null +++ b/docs/tutorial/step_9.md @@ -0,0 +1,171 @@ +--- +title: 'Step 9' +--- + +### Step 9: Polymorphic Resources + +> [View the Diff](https://github.com/graphiti-api/employee_directory/compare/step_8_polymorphic_belongs_to...step_9_polymorphic_resource) + +In the last step, we covered polymorphic relationships: a single +relationship can point to many different Resources. Polymorphic +Resources are the same concept, without an association: a single +Resource can resolve to many different sub-Resources. It's a very similar +to [Single-Table Inheritance in ActiveRecord](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html). + +To illustrate this, we'll add a `tasks` table and corresponding `Task` superclass. Each record in this table will resolve to one of `Bug`, `Epic`, or `Feature`. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
idmilestone_idtypetitle
1nullBugIncorrect Value!
2nullFeatureBuild great stuff!
31EpicBuild TONS of great stuff!
+ +Why not just stick with a single `Task` model? Because each of these types has specific behavior: only `Feature`s have a `points` attribute, and only `Epic`s have a `milestones` relationship. + +### The Rails Stuff 🚂 + +Let's create our `Task` model: + +```bash +$ bin/rails g model Task employee:belongs_to team:belongs_to type:string +title:string +$ bin/rails db:migrate +``` + +And create models to reflect our STI logic: + +```ruby +# app/models/task.rb +class Task < ApplicationRecord + TYPES = %w(Bug Feature Epic) + + belongs_to :team, optional: true + belongs_to :employee, optional: true +end + +# app/models/bug.rb +class Bug < Task +end + +# app/models/feature.rb +class Feature < Task +end + +# Only Epics have Milestones +# app/models/epic.rb +class Epic < Task + has_many :milestones +end + +# app/models/milestone.rb +class Milestone < ApplicationRecord + belongs_to :epic +end +``` + +Add the association: + +```ruby +# app/models/team.rb +has_many :tasks +has_many :bugs +has_many :features +has_many :epics + +# app/models/employee.rb +has_many :tasks +has_many :bugs +has_many :features +has_many :epics +``` + +Finally [view the diff](https://github.com/graphiti-api/employee_directory/compare/step_8_polymorphic_belongs_to...step_9_polymorphic_resource) to edit your `seeds.rb` file. + +### The Graphiti Stuff 🎨 + +Start by creating our Resource as normal: + +```bash +$ bin/rails g graphiti:resource Task title:string +``` + +Now edit to support polymorphism and associations: + +```ruby +class TaskResource < ApplicationResource + self.polymorphic = %w(FeatureResource BugResource EpicResource) + + attribute :employee_id, :integer, only: [:filterable] + attribute :team_id, :integer, only: [:filterable] + attribute :title, :string + + belongs_to :employee + belongs_to :team +end +``` + +The point of this was to show how responses could be specific to type, +so let's customize `Features`: + +```ruby +class FeatureResource < TaskResource + attribute :points, :integer do + rand(20) + end +end +``` + + +Only Epics have milestones, but let's support those as well: + +```bash +$ bin/rails g graphiti:resource Milestone name:string +``` + +```ruby +class MilestoneResource < ApplicationResource + attribute :epic_id, :integer, only: [:filterable] + attribute :name, :string + + # Customize the link to the Tasks endpoint, as we + # didn't create an Epics endpoint + belongs_to :epic do + link do |milestone| + helpers = Rails.application.routes.url_helpers + helpers.task_url(milestone.epic_id) + end + end +end +``` + +#### Digging Deeper 🧐 + +We can now resolve `Tasks`, either as a relationship or through the `/tasks` endpoint directly. When `Task` is type `'Feature'` it will have an extra attribute of `points`. When it's an `Epic`, it will have an additional relationship `Milestone`. + +Graphiti is smart enough to fetch the appropriate relationships. A hit +to `/tasks?include=milestones` will only query for milestones when the resulting `Task` records are `Epic`s. diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 00000000..b190c309 --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,416 @@ +--- +title: 'Upgrading to Graphiti 2.0' +slug: /upgrading +--- + +# Upgrading to Graphiti 2.0 + +Graphiti 2.0 requires **Ruby 3.2+** and **ActiveSupport 7.1+**. Rails is not a dependency, but if you use it, 7.1+. Ruby 3.1 and earlier are past end of life, and Rails 6.1 and 7.0 do not support Ruby 3.2. Apps that cannot move yet should stay on the 1.x branch, which remains open for hotfixes. + +## What you have to change {#what-you-have-to-change} + +Five things, and four of them fail loudly if you skip them. + +**1. Drop three gems.** `graphiti-rails`, `graphiti_spec_helpers` and `graphiti_errors` are now part of `graphiti` itself. + +```diff title="Gemfile" ++ gem "graphiti", "~> 2.0.0.beta" # follows the betas and picks up 2.0 final when it ships +- gem "graphiti-rails" +- gem "graphiti_spec_helpers" +- gem "graphiti_errors" +``` + +Graphiti raises at load if one is still installed, because they ship files that collide with Graphiti's own, so leaving them in place means load order decides which copy you get. + +**2. Include the Rails integration in your controllers.** + +```ruby +class ApplicationController < ActionController::Base + include Graphiti::Rails::Controller +end +``` + +If the controller already has `include Graphiti::Rails`, replace it with `include Graphiti::Rails::Controller`. + +
+What the include actually brings, and what a controller without it loses + +Until 2.0, Graphiti added itself to **every** controller in the application: an `around_action` wrapping each request in a Graphiti context, another wrapping it in the debugger, and a catch-all exception handler, on Devise controllers, admin controllers, HTML pages, everything. + +`Graphiti::Rails::Controller` now bundles all of it, and including it is required. Including it in `ApplicationController` matches 1.x behavior. Including it in an API base class scopes it and leaves the rest of the app alone. A controller without it gets no Graphiti context, no debugger, and none of Graphiti's exception handlers, so if a resource action sees an empty `Graphiti.context`, this include is what is missing. It also carries `ActionController::MimeResponds`, so `respond_to` works in `ActionController::API` apps, which under 1.x only came with `Graphiti::Rails::Responders`. + +The class-level DSL travels with it, which is the one failure you see before a request is ever served: + +```ruby +class PostsController < ApplicationController + self.sideload_allowlist = {index: [:comments]} # NoMethodError without the include +end +``` + +`sideload_allowlist` comes from `Graphiti::Context`, so a controller that never includes `Graphiti::Rails::Controller` raises `NoMethodError` while the class body is being loaded. Watch for base classes that were given `Graphiti::Rails::Responders` alone. Responders declares formats and nothing else, and does not carry the context. + +`Graphiti::Rails::Responders` is separate and most apps do not need it. It exists for the [`responders`](https://github.com/heartcombo/responders) gem's `respond_with`, and depends on that gem, which is why it is not part of `Graphiti::Rails::Controller`. + +
+ +**3. Delete any `rescue_from` that called `handle_exception`**, if you have one. + +```ruby +# 1.x, on a controller that included GraphitiErrors +rescue_from Exception do |e| + handle_exception(e) + Sentry.capture_exception(e) unless registered_exception?(e) +end +``` + +Rendering is middleware's job now, so `handle_exception` is gone and there is nothing left to call. Use `RescueRegistry.handles_exception?` if you still want the check. Exceptions reach your tracker's middleware on their own, and Graphiti's 400s and 404s stay out of `Rails.error` because they sit in Rails' `rescue_responses`. + +**4. Update `around_persistence` hooks**, if you have any. + +They now receive the already-assigned model where they used to receive the attributes hash, so a hook doing `attributes[:tenant_id] = current_tenant.id` raises. Move that to `before_attributes`, or set it on the model. + +
+Before and after, and what else moved with it + +Attributes are now assigned to the model once, up front, before the persistence hooks run, which is what lets `build` and `find` hand you the model before anything is written. See the [lifecycle hooks guide](/concepts/persisting#persistence-lifecycle-hooks) for what that enables. + +That changes one hook. + +#### around_persistence receives the model, not the attributes hash + +It now wraps the save of an already-assigned model, and gets that model: + +```ruby +# 1.x +def do_around_persistence(attributes) + attributes[:tenant_id] = current_tenant.id + model = yield + model.log_saved! +end + +# 2.0 +def do_around_persistence(model) + model.tenant_id = current_tenant.id # last chance to touch the model before save, inside the transaction + saved = yield + saved.log_saved! +end +``` + +To migrate, move attribute-hash modifications to `before_attributes` (which still receives the mutable hash, before assignment), or set the value on the model as above. Hooks that only wrap their yield, such as transactions, timing and post-save side effects, need no changes. Graphiti 1.x releases warn at runtime when a hook would be affected. + +`before/around/after_attributes` and `before/around/after_save` are unchanged. Custom `create`/`update` adapter overrides keep their 1.x signatures. + +#### Fine print + +- If you inspect the model before saving, the attributes callbacks run at inspection time (in the controller) outside the save transaction, and before sideposted parents are persisted. On the plain `save` path they run inside the transaction, at the same point as 1.x. If a hook needs the foreign key of a sideposted parent, use `before_save` instead, which always gets the model with those keys set. +- A writable guard asking for the model gets a fresh build/find, never the current request's unsaved changes. +- Sideposted child models are still built and assigned during save, and `data` exposes the pre-assigned root model only. +- The 1.x runtime warning fires when a hook mutates the hash, which is all it can detect. A hook that only reads it (e.g. Rails.logger.info `attributes[:name]`) gets no warning and now reads the model instead. On ActiveRecord `model[:name]` still answers, but hash-only calls like `attributes.key?`, `dig` or `except` raise. + +
+ +**5. Wrap specs that assert on error payloads.** + +```ruby +RSpec.configure do |config| + config.include Graphiti::Rails::TestHelpers, type: :request +end + +it "renders a 404" do + handle_request_exceptions { get "/posts/999" } + + expect(response.status).to eq(404) +end +``` + +Exceptions now propagate untouched in tests rather than rendering, so a spec expecting a 404 body sees the exception raised instead. This is the one that breaks the suite that would otherwise have told you the app was fine. + +You do not have to edit them one by one. An `around` hook restores 1.x behavior for every request spec, and puts the setting back after each example: + +```ruby +config.around(type: :request) { |example| handle_request_exceptions { example.run } } +``` + +
+Why it has to be a request spec + +It has to be a request spec. Exceptions are rendered in Rack middleware, which controller specs bypass, so the same assertion in a controller spec never sees a rendered payload no matter how it is wrapped. + +`handle_request_exceptions` replaces `GraphitiErrors.enable!` and `.disable!`, which toggled rendering globally. Wrapping the request scopes it to the example instead. + +
+ +## Behavior changes to be aware of {#behavior-changes} + +Nothing to do here. These change what a client gets back, or when a callback runs, and nothing warns you about them the way the renames below do. + +
+A `belongs_to` renders resource ids when its foreign key already holds them, where 1.x sent only a link + +A `belongs_to` now renders resource ids in the payload by default, where 1.x sent only a link: + +```json +"employee": { "data": { "type": "employees", "id": "1" }, "links": { "related": "..." } } +``` + +The id comes from the foreign key already on the parent, so this costs no extra queries. `has_many` is unchanged, since answering there means a query per record. + +Not every `belongs_to` qualifies. A remote target or a custom `primary_key` mean the foreign key is not the related id, a polymorphic target means one rendered type cannot cover every record, and a `scope` or `params` block or a `base_scope` mean the key might not survive the filter. Rendering ids for those means loading the association, so they stay opt-in as in 1.x and render nothing until you ask. + +Run [`bin/rake graphiti:audit`](/topics/debugging#graphiti-audit) to see where your API stands: it lists every relationship that renders no ids, and why. + +To go back to the old payload for one relationship: + +```ruby +belongs_to :employee, resource_ids: false +``` + +Or for the whole API, on the resource everything inherits from: + +```ruby +class ApplicationResource < Graphiti::Resource + self.abstract_class = true + + self.belongs_to_resource_ids_by_default = :never +end +``` + +If you carry the `Sideload::BelongsTo` monkey patch from [#167](https://github.com/graphiti-api/graphiti/issues/167), delete it and set nothing. The default now covers the safe cases on its own. To force ids onto the rest the way the patch did, set `self.belongs_to_resource_ids_by_default = :always`, at a query per record for each one. + +The three settings, and when a `belongs_to` cannot use its foreign key, are covered in [Customizing Relationships](/concepts/relationships#belongs-to-resource-ids). + +
+ +
+A relationship with no ids and no link is left out of the payload, where 1.x rendered meta: {included: false} + +A relationship that renders neither resource ids nor a link used to look like this: + +```json +"employee": { "meta": { "included": false } } +``` + +That shape comes from `jsonapi-serializable`, which fills in a relationship object it would otherwise render empty. It is not part of JSON:API and carries nothing a client can act on. Some clients read it as an empty relationship and clear data they already hold. 1.x left these out too when `links_on_demand` was on globally and the request did not ask for links. + +To keep them, on one resource or on the resource everything inherits from: + +```ruby +self.relationship_placeholders = true +``` + +
+ +
+A request with ?include= always gets an included key back + +When nothing comes back with the response, that key now holds an empty array. 1.x left it out entirely, so a client had to handle both a missing key and an empty one. + +
+ +
+`ConflictRequest` renders `code: "conflict"` at 409, where `graphiti-rails` surfaced it as a 500 + +Graphiti 1.x shipped two exception systems, `graphiti_errors` in core and `rescue_registry` in `graphiti-rails`, and both loaded in every Rails app. `rescue_registry` is now the only one, and installs automatically as a dependency. + +Graphiti registers handlers for `InvalidRequest` (400), `ConflictRequest` (409), `RecordNotFound` (404), `RemoteWrite` (400) and `SingularSideload` (400), plus a fallback that renders anything else as JSON:API. Register your own on any controller: + +```ruby +register_exception MyApp::Forbidden, status: 403 +register_exception MyApp::Throttled, status: 429, handler: MyApp::ThrottleHandler +``` + +`register_exception` comes from `rescue_registry`, which adds it to every controller, so you do not need `Graphiti::Rails::Controller` to register your own exceptions or to have them rendered. What the include adds is Graphiti's own registrations above, plus the fallback that renders anything unregistered as JSON:API. + +Only formats in `config.graphiti.handled_exception_formats` (default `[:jsonapi]`) are rendered by Graphiti. Everything else falls through to Rails. + +Registering one of these classes again replaces Graphiti's, and the last call wins. `graphiti_errors` apps often did, `UnsupportedPageSize` at 422 most of all. Put the include at the top of the class and your own registrations below it. + +If you subclassed `GraphitiErrors::ExceptionHandler`, note the interface changed with the gem: it is now `build_payload` / `formatted_response` / `status_code`, not `error_payload` / `status_code(error)`. + +Registering and customizing handlers is covered in [Error Handling](/topics/error-handling). + +**Conflicts now report as conflicts.** `Graphiti::Errors::ConflictRequest`, raised when a `PATCH` payload's id does not match the URL, used to render a 409 whose body said `code: "bad_request"`, `title: "Request Error"`. It now says `code: "conflict"`, `title: "Conflict Error"`. Under `graphiti-rails` this exception had no registered handler at all and surfaced as a 500, so for most apps this payload is new rather than changed. + +
+ +
+A 500 no longer claims your engineers have been notified + +`rescue_registry` gave every 5xx that detail. Graphiti drops it, so a 500 renders `code`, `status` and `title` alone. To say something there, set a locale key rather than subclassing a handler: + +```yaml +en: + graphiti: + errors: + internal_server_error: + title: "Something went wrong" + detail: "We've probably received an error report already, but please contact us if the issue persists." +``` + +`rails g graphiti:locale` writes the file for you. Keyed by error code, so it works for any status. See [Error Handling](/topics/error-handling#copy). + +
+ +
+`Node#respond_to?` answers `true` for any attribute present in the payload + +`Node#respond_to?` is now a proper `respond_to_missing?`, so `node.respond_to?(:first_name)` returns `true` for attributes present in the payload where it used to return `false`. Nothing to do unless a spec asserted on the old `false`. + +The node helpers are covered in [#jsonapi_data](/topics/testing#jsonapi-data). + +
+ +
+Attributes are assigned before the persistence hooks run, so inspecting a model first moves the attributes callbacks outside the save transaction + +If you inspect the model before saving, the attributes callbacks run at inspection time, in your controller and outside the save transaction. On the plain `save` path they run inside the transaction, at the same point as 1.x. + +The hooks and their order are covered in [Persistence Lifecycle Hooks](/concepts/persisting#persistence-lifecycle-hooks). + +
+ +
+`ActiveSupport::CurrentAttributes` now flow into concurrent sideloads + +Since 1.8, `Current` was empty inside a concurrent sideload, so `Current.user` read nothing in production. It now reads what it did in the controller. Workarounds that resolved `Current` values on the request thread can go. See [Concurrency](/concepts/resources#concurrency). + +
+ +## Deprecations you should fix {#deprecations-you-should-fix} + +Every name below still works, warns, and goes away in the next major. They're all pretty easy fixes though, so why not now? + +### Requires you can delete {#deprecated-requires} + +| 1.x | 2.0 | +| --- | --- | +| `require "graphiti-rails"` | remove / no longer needed | +| `require "graphiti_errors"`, `require "graphiti/responders"` | remove / no longer needed | +| `require "graphiti_spec_helpers/rspec"` | `require "graphiti/spec_helpers/rspec"` | + +### Includes and constants {#deprecated-includes} + +| 1.x | 2.0 | +| --- | --- | +| `include Graphiti::Rails` | `include Graphiti::Rails::Controller`| +| `include Graphiti::Responders` | `include Graphiti::Rails::Responders` | +| `jsonapi_context` | `graphiti_context` | +| `Graphiti::Rails::DEPRECATOR` | `Graphiti::DEPRECATOR` (the old name still resolves) | + +### Request context {#deprecated-context} + +| 1.x | 2.0 | +| --- | --- | +| `context_namespace` | `current_action` | +| `Graphiti.context[:namespace]` | `current_action` | + +### Error serializers {#deprecated-error-serializers} + +| 1.x | 2.0 | +| --- | --- | +| `GraphitiErrors::Validation::Serializer` | `Graphiti::ErrorSerializers::Validation` | +| `GraphitiErrors::InvalidRequest::Serializer` | `Graphiti::ErrorSerializers::InvalidRequest` | +| `GraphitiErrors::ConflictRequest::Serializer` | `Graphiti::ErrorSerializers::ConflictRequest` | + +### Spec helpers {#deprecated-spec-helpers} + +| 1.x | 2.0 | +| --- | --- | +| `GraphitiSpecHelpers::RSpec` / `::Sugar` / `::Errors::*` | `Graphiti::SpecHelpers::*` | +| `include Graphiti::SpecHelpers::Sugar` (`d`, `included`, `errors`, `dt`) | call `jsonapi_data`, `jsonapi_included`, `jsonapi_errors`, `json_datetime` directly | +| rspec shared contexts `"resource testing"`, `"remote api"` | `"graphiti resource testing"`, `"graphiti remote api"` | +| `GraphitiContextProxy` | `Graphiti::SpecHelpers::ContextProxy` | + +### Move these off `Graphiti.config` {#deprecated-global-config} + +These are now resource settings. Set them on `ApplicationResource` to keep the old API-wide behavior, or on individual resources to scope them. + +| 1.x | 2.0 | +| --- | --- | +| `Graphiti.config.links_on_demand = true` | `self.relationship_links = :on_demand` | +| `Graphiti.config.pagination_links = true` | `self.page_links = true` | +| `Graphiti.config.pagination_links_on_demand = true` | `self.page_links = :on_demand` | +| `Graphiti.config.typecast_reads = false` | `self.typecast_reads = false` | + +### Link rendering {#deprecated-links} + +| 1.x | 2.0 | +| --- | --- | +| `self.autolink = false` | `self.relationship_links = false` | + +Link rendering is one mode per link now. It takes `true`, `false`, or `:on_demand`, which renders only when the request asks with `?links=true`. `self.relationship_links` sets the resource default and `link:` overrides it per relationship. + +One behavior shift: `link: true` on a resource now always renders, even when the resource is `:on_demand`. Under the old global `links_on_demand` it stayed hidden until `?links=true`, so change those to `link: :on_demand`. + +### Pagination {#deprecated-pagination} + +| 1.x | 2.0 | +| --- | --- | +| `self.default_page_size = 10` | `self.page_default_size = 10` | +| `self.max_page_size = 500` | `self.page_max_size = 500` | +| `self.cursor_paginatable = true` | `self.page_cursors = true` | + +Everything relating to the `page` param shares its prefix: `page_default_size`, `page_max_size`, `page_cursors` and `page_links`. The on-demand param follows, so use `?page_links=true` (`?pagination_links=true` still works). `page_links` takes the same three modes as `relationship_links`, but has no per-relationship level. + +### Filter blanks {#deprecated-filter-blanks} + +| 1.x | 2.0 | +| --- | --- | +| `self.filters_accept_nil_by_default = true` | `self.filter_blanks_treated_as = :null` | +| `self.filters_deny_empty_by_default = true` | `self.filter_blanks_treated_as = :rejected` | +| `filter :name, allow_nil: true` | `filter :name, blanks: :null` | +| `filter :name, deny_empty: true` | `filter :name, blanks: :rejected` | + +`allow_nil:` and `deny_empty:` were two booleans answering one question, and they contradicted each other on `"null"`. The empty check raised before the coercion could run. One `blanks:` option replaces them, taking `:literal`, `:null` or `:rejected`, defaulted by `filter_blanks_treated_as`. + +### Endpoint validation {#deprecated-endpoint-validation} + +| 1.x | 2.0 | +| --- | --- | +| `self.validate_endpoints = false` | `self.validate_requests = false`, `self.validate_links = false` | + +`validate_endpoints` did two unrelated jobs, so it split. `validate_requests` refuses requests to undeclared endpoints, and `validate_links` refuses to render links to unroutable ones. The old name sets both, and turning off link validation no longer disarms the inbound guard. + +### Relationship resource ids {#deprecated-resource-ids} + +| 1.x | 2.0 | +| --- | --- | +| `always_include_resource_ids: true` on a relationship | `resource_ids: true` | + +The resource-wide version of this setting was removed rather than deprecated. See [`always_include_resource_ids_by_default`](#removed-outright) below. + +## Removed outright {#removed-outright} + +| 1.x | 2.0 | +| --- | --- | +| `include GraphitiErrors` | `include Graphiti::Rails::Controller`, and `register_exception` is on every controller either way | +| `GraphitiErrors::ExceptionHandler` | subclass `Graphiti::Rails::ExceptionHandler` | +| `GraphitiErrors.enable!` / `.disable!` | `handle_request_exceptions` | +| `self.always_include_resource_ids_by_default` | `self.belongs_to_resource_ids_by_default`, which takes `:foreign_key`, `:always` or `:never` | +| `Adapters::ActiveRecord#create` / `#update` | override `#save` | + +`always_include_resource_ids_by_default` raises `NoMethodError` at class-definition time. It applied to every relationship type, and only a `belongs_to` can render resource ids without loading an association, so the replacement covers `belongs_to` alone. `= false` becomes `:never`. There is no equivalent of `= true`, because arming every collection API-wide is the behavior it was removed for. Use `:always` for `belongs_to`. + +## Without Rails {#without-rails} + +
+Using the error serializers and exception handling outside Rails + +The serializers move but keep working: `Graphiti::ErrorSerializers::Validation`, `::InvalidRequest` and `::ConflictRequest` load with core and need no Rails. + +`GraphitiErrors::ExceptionHandler`, which turned any exception into a JSON:API errors payload, is replaced by `RescueRegistry::ExceptionHandler`, a runtime dependency now, and usable outside Rails: + +```ruby +require "rack" # or RescueRegistry::ExceptionHandler raises NameError on Rack +require "rescue_registry" + +handler = RescueRegistry::ExceptionHandler.new(exception, status: 404) +handler.build_payload # => {errors: [{code: :not_found, status: "404", ...}]} +handler.formatted_response(:json) # => [404, "{\"errors\":[...]}", :json] +``` + +`register_exception` and the rendering are Rails-only, but rescue_registry ships `RescueRegistry::ShowExceptions`, a Rack middleware for exactly this case. See its README. + +`GraphitiErrors.logger` has no replacement. `Graphiti.logger` is the nearest thing. + +
diff --git a/examples/plain_ruby/Gemfile b/examples/plain_ruby/Gemfile new file mode 100644 index 00000000..97fbe42b --- /dev/null +++ b/examples/plain_ruby/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +gem "graphiti", path: "../.." +gem "activerecord" +gem "sqlite3" +gem "kaminari" diff --git a/examples/plain_ruby/README.md b/examples/plain_ruby/README.md new file mode 100644 index 00000000..fee25e02 --- /dev/null +++ b/examples/plain_ruby/README.md @@ -0,0 +1,10 @@ +# Graphiti in a plain .rb file + +Graphiti with nothing but Ruby: an in-memory sqlite database, three Resources, and one script that queries with filtering, sorting, pagination and nested sideloads, then renders JSON:API, plain JSON and XML. + +```bash +bundle install +bundle exec ruby index.rb +``` + +The Gemfile points at the gem source two directories up, so this always runs against the checkout you're in. diff --git a/examples/plain_ruby/index.rb b/examples/plain_ruby/index.rb new file mode 100644 index 00000000..8136bf6a --- /dev/null +++ b/examples/plain_ruby/index.rb @@ -0,0 +1,50 @@ +require "pp" +require "active_record" +require "kaminari" +require "graphiti" +require "./seeds" + +class ApplicationResource < Graphiti::Resource + self.abstract_class = true + self.adapter = Graphiti::Adapters::ActiveRecord + self.autolink = false +end + +class EmployeeResource < ApplicationResource + attribute :first_name, :string + attribute :last_name, :string + attribute :age, :integer + + has_many :positions +end + +class PositionResource < ApplicationResource + attribute :employee_id, :integer, only: [:filterable] + attribute :department_id, :integer, only: [:filterable] + attribute :title, :string + + belongs_to :department +end + +class DepartmentResource < ApplicationResource + attribute :name, :string +end + +Graphiti.setup! + +employees = EmployeeResource.all({ + sort: "-id", + filter: {age: {gt: 30}}, + page: {size: 10, number: 1}, + include: "positions.department" +}) + +employees.each do |e| + puts "#{e.first_name} | #{e.positions[0].title} | #{e.positions[0].department.name}" +end + +pp JSON.parse(employees.to_jsonapi) +puts "\n\n" +pp JSON.parse(employees.to_json) +puts "\n\n" +puts employees.to_xml diff --git a/examples/plain_ruby/seeds.rb b/examples/plain_ruby/seeds.rb new file mode 100644 index 00000000..de9c9bc9 --- /dev/null +++ b/examples/plain_ruby/seeds.rb @@ -0,0 +1,49 @@ +ActiveRecord::Base.establish_connection adapter: "sqlite3", + database: ":memory:" + +ActiveRecord::Migration.verbose = false +ActiveRecord::Schema.define(version: 1) do + create_table :employees do |t| + t.string :first_name + t.string :last_name + t.integer :age + end + + create_table :positions do |t| + t.integer :employee_id + t.integer :department_id + t.string :title + end + + create_table :departments do |t| + t.string :name + end +end + +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end + +class Employee < ApplicationRecord + has_many :positions +end + +class Position < ApplicationRecord + belongs_to :employee + belongs_to :department +end + +class Department < ApplicationRecord + has_many :positions +end + +e = Employee.create!(first_name: "Walter", last_name: "White", age: 50) +d = Department.create!(name: "Product") +Position.create!(title: "Cook", department: d, employee: e) + +e = Employee.create!(first_name: "Jesse", last_name: "Pinkman", age: 23) +Position.create!(title: "Junior Cook", department: d, employee: e) + +d = Department.create!(name: "Legal") +e = Employee.create!(first_name: "Saul", last_name: "Goodman", age: 47) +Position.create!(title: "Lawyer", department: d, employee: e) diff --git a/examples/rails/README.md b/examples/rails/README.md new file mode 100644 index 00000000..1316bfa5 --- /dev/null +++ b/examples/rails/README.md @@ -0,0 +1,9 @@ +# Graphiti on Rails + +The full Rails example lives in its own repo: [graphiti-api/employee_directory](https://github.com/graphiti-api/employee_directory) — a complete API with Resources, relationships, generated specs, and the [Vandal](https://graphiti.dev/reference/vandal) schema explorer. + +It's a standalone repo because it's a real, runnable application with its own database and test suite. To start a new app of your own instead, use the [application template](https://graphiti.dev/getting-started/installation): + +```bash +rails new blog --api -m https://raw.githubusercontent.com/graphiti-api/graphiti/main/templates/rails/all.rb +``` diff --git a/examples/sinatra/Gemfile b/examples/sinatra/Gemfile new file mode 100644 index 00000000..c6e9292a --- /dev/null +++ b/examples/sinatra/Gemfile @@ -0,0 +1,10 @@ +source "https://rubygems.org" + +gem "graphiti", path: "../.." +gem "sinatra" +gem "rackup" +gem "puma" +gem "activerecord" +gem "sqlite3" +gem "kaminari" +gem "rack-test" diff --git a/examples/sinatra/README.md b/examples/sinatra/README.md new file mode 100644 index 00000000..528f3df2 --- /dev/null +++ b/examples/sinatra/README.md @@ -0,0 +1,22 @@ +# Graphiti on Sinatra + +Graphiti serving HTTP without Rails: the same Resources as the plain-ruby example, exposed over JSON:API endpoints by a Sinatra app, with `rescue_registry` rendering not-found errors as JSON:API. + +```bash +bundle install +bundle exec rackup +``` + +Then visit [http://localhost:9292](http://localhost:9292) for a small explorer page that fetches each endpoint live, or curl directly: + +```bash +curl -g 'localhost:9292/api/v1/employees?filter[age][gt]=30&include=positions.department' +``` + +Run the smoke test (what CI runs): + +```bash +bundle exec ruby smoke.rb +``` + +The Gemfile points at the gem source two directories up, so this always runs against the checkout you're in. diff --git a/examples/sinatra/app.rb b/examples/sinatra/app.rb new file mode 100644 index 00000000..46d137c5 --- /dev/null +++ b/examples/sinatra/app.rb @@ -0,0 +1,61 @@ +require "sinatra/base" +require "active_record" +require "kaminari" +require "graphiti" +require "rescue_registry" +require_relative "seeds" + +class ApplicationResource < Graphiti::Resource + self.abstract_class = true + self.adapter = Graphiti::Adapters::ActiveRecord + self.endpoint_namespace = "/api/v1" + self.autolink = false +end + +class EmployeeResource < ApplicationResource + attribute :first_name, :string + attribute :last_name, :string + attribute :age, :integer + + has_many :positions +end + +class PositionResource < ApplicationResource + attribute :employee_id, :integer, only: [:filterable] + attribute :department_id, :integer, only: [:filterable] + attribute :title, :string + + belongs_to :department +end + +class DepartmentResource < ApplicationResource + attribute :name, :string +end + +Graphiti.setup! + +class EmployeeDirectory < Sinatra::Base + set :show_exceptions, :after_handler + + before "/api/*" do + content_type "application/vnd.api+json" + end + + get "/" do + send_file File.join(__dir__, "public", "index.html") + end + + get "/api/v1/employees" do + EmployeeResource.all(params).to_jsonapi + end + + get "/api/v1/employees/:id" do + EmployeeResource.find(params).to_jsonapi + end + + error Graphiti::Errors::RecordNotFound do + handler = RescueRegistry::ExceptionHandler.new(env["sinatra.error"], status: 404) + status 404 + handler.build_payload.to_json + end +end diff --git a/examples/sinatra/config.ru b/examples/sinatra/config.ru new file mode 100644 index 00000000..ee4e813b --- /dev/null +++ b/examples/sinatra/config.ru @@ -0,0 +1,2 @@ +require_relative "app" +run EmployeeDirectory diff --git a/examples/sinatra/public/index.html b/examples/sinatra/public/index.html new file mode 100644 index 00000000..92ab8c4e --- /dev/null +++ b/examples/sinatra/public/index.html @@ -0,0 +1,50 @@ + + + + +Graphiti on Sinatra + + + +

Graphiti on Sinatra

+

Graphiti serving JSON:API without Rails. Click an endpoint to fetch it live — filtering, sorting, pagination and sideloads all come from the Resource definitions in app.rb.

+
    +
  • +
  • +
  • +
  • +
  • +
  • +
+
Response appears here.
+

+
+
+
diff --git a/examples/sinatra/seeds.rb b/examples/sinatra/seeds.rb
new file mode 100644
index 00000000..466b9425
--- /dev/null
+++ b/examples/sinatra/seeds.rb
@@ -0,0 +1,52 @@
+# A file-backed database, rebuilt on every boot. An in-memory one won't work
+# here: sqlite scopes :memory: to a single connection, and puma's request
+# threads each get their own from the pool.
+ActiveRecord::Base.establish_connection adapter: "sqlite3",
+  database: File.join(__dir__, "employee_directory.sqlite3")
+
+ActiveRecord::Migration.verbose = false
+ActiveRecord::Schema.define(version: 1) do
+  create_table :employees, force: true do |t|
+    t.string :first_name
+    t.string :last_name
+    t.integer :age
+  end
+
+  create_table :positions, force: true do |t|
+    t.integer :employee_id
+    t.integer :department_id
+    t.string :title
+  end
+
+  create_table :departments, force: true do |t|
+    t.string :name
+  end
+end
+
+class ApplicationRecord < ActiveRecord::Base
+  self.abstract_class = true
+end
+
+class Employee < ApplicationRecord
+  has_many :positions
+end
+
+class Position < ApplicationRecord
+  belongs_to :employee
+  belongs_to :department
+end
+
+class Department < ApplicationRecord
+  has_many :positions
+end
+
+e = Employee.create!(first_name: "Walter", last_name: "White", age: 50)
+d = Department.create!(name: "Product")
+Position.create!(title: "Cook", department: d, employee: e)
+
+e = Employee.create!(first_name: "Jesse", last_name: "Pinkman", age: 23)
+Position.create!(title: "Junior Cook", department: d, employee: e)
+
+d = Department.create!(name: "Legal")
+e = Employee.create!(first_name: "Saul", last_name: "Goodman", age: 47)
+Position.create!(title: "Lawyer", department: d, employee: e)
diff --git a/examples/sinatra/smoke.rb b/examples/sinatra/smoke.rb
new file mode 100644
index 00000000..1fbac5d5
--- /dev/null
+++ b/examples/sinatra/smoke.rb
@@ -0,0 +1,42 @@
+require "rack/test"
+require "json"
+require_relative "app"
+
+class Smoke
+  include Rack::Test::Methods
+
+  def app
+    EmployeeDirectory
+  end
+
+  def assert(condition, message)
+    raise message unless condition
+  end
+
+  def run
+    get "http://localhost/"
+    assert last_response.status == 200, "expected 200 from the index page"
+    assert last_response.body.include?("Graphiti on Sinatra"), "expected the explorer page"
+    assert last_response.content_type.include?("text/html"), "browsers won't render the page as #{last_response.content_type}"
+
+    get "http://localhost/api/v1/employees", filter: {age: {gt: 30}}, sort: "-id", include: "positions.department"
+    assert last_response.status == 200, "expected 200, got #{last_response.status}"
+    payload = JSON.parse(last_response.body)
+    names = payload["data"].map { |d| d["attributes"]["first_name"] }
+    assert names == ["Saul", "Walter"], "expected Saul and Walter, got #{names}"
+    included = payload["included"].map { |i| i["type"] }.sort.uniq
+    assert included == ["departments", "positions"], "expected sideloaded positions and departments"
+
+    get "http://localhost/api/v1/employees/1"
+    assert last_response.status == 200, "expected 200, got #{last_response.status}"
+    assert last_response.content_type == "application/vnd.api+json", "expected the JSON:API content type, got #{last_response.content_type}"
+
+    get "http://localhost/api/v1/employees/999"
+    assert last_response.status == 404, "expected 404, got #{last_response.status}"
+    assert JSON.parse(last_response.body).key?("errors"), "expected a JSON:API errors payload"
+
+    puts "Sinatra example OK"
+  end
+end
+
+Smoke.new.run
diff --git a/gemfiles/rails_5_2.gemfile b/gemfiles/rails_5_2.gemfile
deleted file mode 100644
index 38872d68..00000000
--- a/gemfiles/rails_5_2.gemfile
+++ /dev/null
@@ -1,18 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "rails", "~> 5.2.0"
-gem "rspec-rails"
-gem "sqlite3", "~> 1.4.0"
-gem "database_cleaner"
-
-group :test do
-  gem "pry"
-  gem "pry-byebug", platform: [:mri]
-  gem "appraisal"
-  gem "guard"
-  gem "guard-rspec"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_5_2_graphiti_rails.gemfile b/gemfiles/rails_5_2_graphiti_rails.gemfile
deleted file mode 100644
index 0f483569..00000000
--- a/gemfiles/rails_5_2_graphiti_rails.gemfile
+++ /dev/null
@@ -1,19 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "rails", "~> 5.2.0"
-gem "rspec-rails"
-gem "sqlite3", "~> 1.4.0"
-gem "database_cleaner"
-gem "graphiti-rails", "~> 0.4.0"
-
-group :test do
-  gem "pry"
-  gem "pry-byebug", platform: [:mri]
-  gem "appraisal"
-  gem "guard"
-  gem "guard-rspec"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_6.gemfile b/gemfiles/rails_6.gemfile
deleted file mode 100644
index 8d8f1fb3..00000000
--- a/gemfiles/rails_6.gemfile
+++ /dev/null
@@ -1,18 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "rails", "~> 6.0"
-gem "rspec-rails"
-gem "sqlite3", "~> 1.4.0"
-gem "database_cleaner"
-
-group :test do
-  gem "pry"
-  gem "pry-byebug", platform: [:mri]
-  gem "appraisal"
-  gem "guard"
-  gem "guard-rspec"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_6_graphiti_rails.gemfile b/gemfiles/rails_6_graphiti_rails.gemfile
deleted file mode 100644
index ea204ff2..00000000
--- a/gemfiles/rails_6_graphiti_rails.gemfile
+++ /dev/null
@@ -1,19 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "rails", "~> 6.0"
-gem "rspec-rails"
-gem "sqlite3", "~> 1.4.0"
-gem "database_cleaner"
-gem "graphiti-rails", "~> 0.4.0"
-
-group :test do
-  gem "pry"
-  gem "pry-byebug", platform: [:mri]
-  gem "appraisal"
-  gem "guard"
-  gem "guard-rspec"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_7.gemfile b/gemfiles/rails_7.gemfile
deleted file mode 100644
index 054b43bb..00000000
--- a/gemfiles/rails_7.gemfile
+++ /dev/null
@@ -1,18 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "rails", "~> 7.0"
-gem "rspec-rails"
-gem "sqlite3", "~> 1.4.0"
-gem "database_cleaner"
-
-group :test do
-  gem "pry"
-  gem "pry-byebug", platform: [:mri]
-  gem "appraisal"
-  gem "guard"
-  gem "guard-rspec"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile
index ef55184f..9d72cd39 100644
--- a/gemfiles/rails_7_1.gemfile
+++ b/gemfiles/rails_7_1.gemfile
@@ -2,12 +2,13 @@
 
 source "https://rubygems.org"
 
-gem "rails", "~> 7.1"
+gem "rails", "~> 7.1.0"
 gem "rspec-rails"
-gem "sqlite3", "~> 2.1"
-gem "database_cleaner"
+gem "responders"
+gem "sqlite3", "~> 1.4"
 
 group :test do
+  gem "database_cleaner"
   gem "pry"
   gem "pry-byebug", platform: [:mri]
   gem "appraisal"
diff --git a/gemfiles/rails_7_1_graphiti_rails.gemfile b/gemfiles/rails_7_1_graphiti_rails.gemfile
deleted file mode 100644
index 97c0a5e5..00000000
--- a/gemfiles/rails_7_1_graphiti_rails.gemfile
+++ /dev/null
@@ -1,19 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "rails", "~> 7.1"
-gem "rspec-rails"
-gem "sqlite3", "~> 1.4.0"
-gem "database_cleaner"
-gem "graphiti-rails", "~> 0.4.0"
-
-group :test do
-  gem "pry"
-  gem "pry-byebug", platform: [:mri]
-  gem "appraisal"
-  gem "guard"
-  gem "guard-rspec"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_8_0_graphiti_rails.gemfile b/gemfiles/rails_7_2.gemfile
similarity index 76%
rename from gemfiles/rails_8_0_graphiti_rails.gemfile
rename to gemfiles/rails_7_2.gemfile
index 46a3c4ce..4e288bb6 100644
--- a/gemfiles/rails_8_0_graphiti_rails.gemfile
+++ b/gemfiles/rails_7_2.gemfile
@@ -2,13 +2,13 @@
 
 source "https://rubygems.org"
 
-gem "rails", "~> 8.0"
+gem "rails", "~> 7.2.0"
 gem "rspec-rails"
+gem "responders"
 gem "sqlite3", "~> 2.1"
-gem "database_cleaner"
-gem "graphiti-rails", "~> 0.4.0"
 
 group :test do
+  gem "database_cleaner"
   gem "pry"
   gem "pry-byebug", platform: [:mri]
   gem "appraisal"
diff --git a/gemfiles/rails_7_graphiti_rails.gemfile b/gemfiles/rails_7_graphiti_rails.gemfile
deleted file mode 100644
index 0aae1f4d..00000000
--- a/gemfiles/rails_7_graphiti_rails.gemfile
+++ /dev/null
@@ -1,19 +0,0 @@
-# This file was generated by Appraisal
-
-source "https://rubygems.org"
-
-gem "rails", "~> 7.0"
-gem "rspec-rails"
-gem "sqlite3", "~> 1.4.0"
-gem "database_cleaner"
-gem "graphiti-rails", "~> 0.4.0"
-
-group :test do
-  gem "pry"
-  gem "pry-byebug", platform: [:mri]
-  gem "appraisal"
-  gem "guard"
-  gem "guard-rspec"
-end
-
-gemspec path: "../"
diff --git a/gemfiles/rails_8_1_graphiti_rails.gemfile b/gemfiles/rails_8_0.gemfile
similarity index 76%
rename from gemfiles/rails_8_1_graphiti_rails.gemfile
rename to gemfiles/rails_8_0.gemfile
index a15c6c20..a6860e3d 100644
--- a/gemfiles/rails_8_1_graphiti_rails.gemfile
+++ b/gemfiles/rails_8_0.gemfile
@@ -2,13 +2,13 @@
 
 source "https://rubygems.org"
 
-gem "rails", "~> 8.1"
+gem "rails", "~> 8.0.0"
 gem "rspec-rails"
+gem "responders"
 gem "sqlite3", "~> 2.1"
-gem "database_cleaner"
-gem "graphiti-rails", "~> 0.4.0"
 
 group :test do
+  gem "database_cleaner"
   gem "pry"
   gem "pry-byebug", platform: [:mri]
   gem "appraisal"
diff --git a/gemfiles/rails_7_2_graphiti_rails.gemfile b/gemfiles/rails_8_1.gemfile
similarity index 76%
rename from gemfiles/rails_7_2_graphiti_rails.gemfile
rename to gemfiles/rails_8_1.gemfile
index 4da00bb1..33206d1a 100644
--- a/gemfiles/rails_7_2_graphiti_rails.gemfile
+++ b/gemfiles/rails_8_1.gemfile
@@ -2,13 +2,13 @@
 
 source "https://rubygems.org"
 
-gem "rails", "~> 7.2"
+gem "rails", "~> 8.1.0"
 gem "rspec-rails"
+gem "responders"
 gem "sqlite3", "~> 2.1"
-gem "database_cleaner"
-gem "graphiti-rails", "~> 0.4.0"
 
 group :test do
+  gem "database_cleaner"
   gem "pry"
   gem "pry-byebug", platform: [:mri]
   gem "appraisal"
diff --git a/graphiti.gemspec b/graphiti.gemspec
index 322e8682..b587feb3 100644
--- a/graphiti.gemspec
+++ b/graphiti.gemspec
@@ -12,26 +12,32 @@ Gem::Specification.new do |spec|
   spec.homepage = "https://github.com/graphiti-api/graphiti"
   spec.license = "MIT"
 
-  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
+  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|templates|website|docs|examples)/}) }
   spec.bindir = "exe"
   spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
   spec.require_paths = ["lib"]
-  spec.required_ruby_version = ">= 2.7"
+  spec.required_ruby_version = ">= 3.2"
 
   spec.add_dependency "jsonapi-serializable", "~> 0.3.0"
   spec.add_dependency "jsonapi-renderer", "~> 0.2", ">= 0.2.2"
   spec.add_dependency "dry-types", ">= 0.15.0", "< 2.0"
-  spec.add_dependency "graphiti_errors", "~> 1.1.0"
+  spec.add_dependency "rescue_registry", "~> 1.1"
   spec.add_dependency "concurrent-ruby", ">= 1.2", "< 2.0"
-  spec.add_dependency "activesupport", ">= 5.2"
-  # Bundled (no longer default) as of Ruby 3.5; graphiti uses OpenStruct in lib/
+  spec.add_dependency "activesupport", ">= 7.1"
+  # Bundled (no longer default) as of Ruby 4.0; graphiti uses OpenStruct in lib/
   spec.add_dependency "ostruct", ">= 0.5"
 
-  spec.add_development_dependency "faraday", "~> 0.15"
-  spec.add_development_dependency "kaminari", "~> 0.17"
+  spec.add_development_dependency "faraday", ">= 2.0", "< 3"
+  spec.add_development_dependency "kaminari", "~> 1.2"
   spec.add_development_dependency "bundler"
   spec.add_development_dependency "rake", ">= 10.0"
-  spec.add_development_dependency "standard", "~> 1.4.0"
-  spec.add_development_dependency "activemodel", ">= 5.2"
-  spec.add_development_dependency "graphiti_spec_helpers", "1.0.beta.4"
+  spec.add_development_dependency "standard", "~> 1.56"
+  spec.add_development_dependency "activemodel", ">= 7.1"
+  # Only the specs need this, for ActiveSupport's Hash.from_xml when asserting
+  # on to_xml output. It used to arrive transitively via rubocop; rubocop 1.88
+  # dropped it, and it is a bundled (not default) gem since Ruby 3.0.
+  spec.add_development_dependency "rexml"
+  # Was reaching the suite transitively through graphiti_spec_helpers, which is
+  # now part of this gem.
+  spec.add_development_dependency "rspec", "~> 3.0"
 end
diff --git a/deprecated_generators/graphiti/api_test_generator.rb b/lib/generators/graphiti/api_test_generator.rb
similarity index 89%
rename from deprecated_generators/graphiti/api_test_generator.rb
rename to lib/generators/graphiti/api_test_generator.rb
index 1d4a26ac..7aa7cbee 100644
--- a/deprecated_generators/graphiti/api_test_generator.rb
+++ b/lib/generators/graphiti/api_test_generator.rb
@@ -4,7 +4,7 @@ module Graphiti
   class ApiTestGenerator < ::Rails::Generators::Base
     include GeneratorMixin
 
-    source_root File.expand_path("templates", __dir__)
+    source_root File.expand_path("../templates", __FILE__)
 
     argument :resource, type: :string
     class_option :actions,
@@ -13,6 +13,12 @@ class ApiTestGenerator < ::Rails::Generators::Base
       aliases: ["--actions", "-a"],
       desc: 'Array of controller actions, e.g. "index show destroy"'
 
+    class_option :rawid,
+      type: :boolean,
+      default: false,
+      aliases: ["--rawid", "-r"],
+      desc: "Generate tests using rawid"
+
     desc "Generates rspec request specs at spec/api"
     def generate
       generate_api_specs
diff --git a/lib/generators/graphiti/generator_mixin.rb b/lib/generators/graphiti/generator_mixin.rb
new file mode 100644
index 00000000..0bca49eb
--- /dev/null
+++ b/lib/generators/graphiti/generator_mixin.rb
@@ -0,0 +1,84 @@
+module Graphiti
+  module GeneratorMixin
+    def prompt(header: nil, description: nil, default: nil)
+      say(set_color("\n#{header}", :magenta, :bold)) if header
+      say("\n#{description}") if description
+      answer = ask(set_color("\n(default: #{default}):", :magenta, :bold))
+      answer = default if answer.blank? && default != "nil"
+      say(set_color("\nGot it!\n", :white, :bold))
+      answer
+    end
+
+    def api_namespace
+      @api_namespace ||= begin
+        ns = graphiti_config["namespace"]
+
+        if ns.blank?
+          ns = prompt \
+            header: "What is your API namespace?",
+            description: "This will be used as a route prefix, e.g. if you want the route '/books_api/v1/authors' your namespace would be '/books_api/v1'",
+            default: "/api/v1"
+          update_config!("namespace" => ns)
+        end
+
+        ns
+      end
+    end
+
+    def actions
+      @options["actions"] || %w[index show create update destroy]
+    end
+
+    def actions?(*methods)
+      methods.any? { |m| actions.include?(m) }
+    end
+
+    def resource_setting_groups
+      name_width = Graphiti::Resource::SETTINGS.keys.map(&:length).max
+      assignments = Graphiti::Resource::SETTINGS.to_h do |name, setting|
+        value = setting[:format] || setting[:default].inspect
+        [name, "self.#{name.to_s.ljust(name_width)} = #{value}"]
+      end
+      hint_column = assignments.values.map(&:length).max + 2
+
+      Graphiti::Resource::SETTING_GROUPS.transform_values do |settings|
+        settings.map do |name, setting|
+          hint = resource_setting_hint(setting)
+          hint ? "#{assignments[name].ljust(hint_column)}# #{hint}" : assignments[name]
+        end
+      end
+    end
+
+    def resource_setting_hint(setting)
+      hint = setting[:note] || setting[:values]&.map(&:inspect)
+        &.to_sentence(two_words_connector: " or ", last_word_connector: ", or ")
+
+      return hint unless setting[:deprecated]
+
+      ["deprecated, removed in 3.0", hint].compact.join(". ")
+    end
+
+    def graphiti_config
+      File.exist?(".graphiticfg.yml") ? YAML.load_file(".graphiticfg.yml") : {}
+    end
+
+    def update_config!(attrs)
+      config = graphiti_config.merge(attrs)
+      File.write(".graphiticfg.yml", config.to_yaml)
+    end
+
+    def id_or_rawid
+      @options["rawid"] ? "rawid" : "id"
+    end
+
+    def sort_raw_ids
+      return unless @options["rawid"]
+      ".sort"
+    end
+
+    def sort_raw_ids_descending
+      return unless @options["rawid"]
+      ".sort.reverse"
+    end
+  end
+end
diff --git a/lib/generators/graphiti/install_generator.rb b/lib/generators/graphiti/install_generator.rb
new file mode 100644
index 00000000..0ef65fb6
--- /dev/null
+++ b/lib/generators/graphiti/install_generator.rb
@@ -0,0 +1,75 @@
+require_relative "generator_mixin"
+require_relative "locale_generator"
+
+module Graphiti
+  class InstallGenerator < ::Rails::Generators::Base
+    include GeneratorMixin
+
+    source_root File.expand_path("templates", __dir__)
+
+    class_option :"omit-comments",
+      type: :boolean,
+      default: false,
+      aliases: ["-c"],
+      desc: "Generate without documentation comments"
+
+    desc "This generator boostraps graphiti"
+    def install
+      to = File.join("app/resources", "application_resource.rb")
+      template("application_resource.rb.erb", to)
+
+      invoke(Graphiti::LocaleGenerator, [], options)
+
+      inject_into_file "app/controllers/application_controller.rb", after: "class ApplicationController < ActionController::API\n" do
+        app_controller_code
+      end
+
+      inject_into_file "app/controllers/application_controller.rb", after: "class ApplicationController < ActionController::Base\n" do
+        app_controller_code
+      end
+
+      # Thor aborts the whole generator when this file is missing.
+      if defined?(RSpec) && File.exist?(File.join(destination_root, "spec/rails_helper.rb"))
+        inject_into_file "spec/rails_helper.rb", after: /RSpec.configure.+^end$/m do
+          <<~RUBY
+
+            require "graphiti/spec_helpers/rspec"
+
+            RSpec.configure do |config|
+              config.include Graphiti::SpecHelpers::RSpec
+            end
+
+            Graphiti::SpecHelpers::RSpec.schema!
+          RUBY
+        end
+      end
+
+      insert_into_file "config/routes.rb", after: "Rails.application.routes.draw do\n" do
+        namespace_scope
+      end
+    end
+
+    private
+
+    def omit_comments?
+      @options["omit-comments"]
+    end
+
+    # The resource generator re-finds this scope by its format default.
+    def namespace_scope
+      lines = ["  scope path: \"#{api_namespace}\", defaults: {format: :jsonapi} do\n"]
+      lines << "    mount VandalUi::Engine, at: '/vandal'\n" if defined?(VandalUi)
+      lines << "    # your routes go here\n"
+      lines << "  end\n"
+      lines.join
+    end
+
+    def app_controller_code
+      str = +"  include Graphiti::Rails::Controller\n"
+      if defined?(::Responders)
+        str << "  include Graphiti::Rails::Responders\n"
+      end
+      str
+    end
+  end
+end
diff --git a/lib/generators/graphiti/locale_generator.rb b/lib/generators/graphiti/locale_generator.rb
new file mode 100644
index 00000000..932d4331
--- /dev/null
+++ b/lib/generators/graphiti/locale_generator.rb
@@ -0,0 +1,86 @@
+module Graphiti
+  class LocaleGenerator < ::Rails::Generators::Base
+    source_root File.expand_path("templates", __dir__)
+
+    class_option :"omit-comments",
+      type: :boolean,
+      default: false,
+      aliases: ["-c"],
+      desc: "Generate without documentation comments"
+
+    # Every code Graphiti renders a title for, and the title it renders today.
+    ERROR_TITLES = {
+      bad_request: "Request Error",
+      not_found: "Not Found",
+      conflict: "Conflict Error",
+      unprocessable_entity: "Validation Error",
+      internal_server_error: "Internal Server Error"
+    }.freeze
+
+    # The two codes no registration produces.
+    ERROR_SOURCES = {
+      unprocessable_entity: ["a write that failed model validations"],
+      internal_server_error: ["any exception Graphiti did not register"]
+    }.freeze
+
+    # Written into the app's locale file, not defaulted in Graphiti.
+    ERROR_DETAILS = {
+      internal_server_error: "We've probably received an error report already, but please contact us if the issue persists."
+    }.freeze
+
+    COMMENT_WIDTH = 68
+
+    desc "Writes the error text Graphiti renders, ready to edit"
+    def locale
+      template("locale.yml.erb", File.join("config/locales", "graphiti.en.yml"))
+    end
+
+    private
+
+    def omit_comments?
+      @options["omit-comments"]
+    end
+
+    def error_titles
+      ERROR_TITLES
+    end
+
+    def error_detail(code)
+      ERROR_DETAILS[code]
+    end
+
+    # What raises this code, so the file says where its text is used.
+    def error_source_lines(code)
+      ERROR_SOURCES[code] || wrap(exception_names(code))
+    end
+
+    def exception_names(code)
+      Graphiti::Rails::CLIENT_ERROR_STATUSES
+        .select { |_exception, status| status == code }
+        .keys.map { |exception| exception.delete_prefix("Graphiti::Errors::") }
+    end
+
+    def wrap(names)
+      items = names.map.with_index do |name, index|
+        (index == names.length - 1) ? name : "#{name},"
+      end
+
+      items.each_with_object([]) do |item, lines|
+        joined = "#{lines.last} #{item}"
+        if lines.empty? || joined.length > COMMENT_WIDTH
+          lines << item
+        else
+          lines[-1] = joined
+        end
+      end
+    end
+
+    def default_messages
+      Graphiti::Util::SimpleErrors::DEFAULT_MESSAGES
+    end
+
+    def default_format
+      Graphiti::Util::SimpleErrors::DEFAULT_FORMAT
+    end
+  end
+end
diff --git a/deprecated_generators/graphiti/resource_generator.rb b/lib/generators/graphiti/resource_generator.rb
similarity index 63%
rename from deprecated_generators/graphiti/resource_generator.rb
rename to lib/generators/graphiti/resource_generator.rb
index 5129d53b..5566c3bd 100644
--- a/deprecated_generators/graphiti/resource_generator.rb
+++ b/lib/generators/graphiti/resource_generator.rb
@@ -8,24 +8,36 @@ class ResourceGenerator < ::Rails::Generators::NamedBase
 
     argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
 
-    class_option :'omit-comments',
+    class_option :"omit-comments",
       type: :boolean,
       default: false,
       aliases: ["--omit-comments", "-c"],
       desc: "Generate without documentation comments"
 
+    class_option :rawid,
+      type: :boolean,
+      default: false,
+      aliases: ["--rawid", "-r"],
+      desc: "Generate tests using rawid"
+
     class_option :actions,
       type: :array,
       default: nil,
       aliases: ["--actions", "-a"],
       desc: 'Array of controller actions to support, e.g. "index show destroy"'
 
-    class_option :'attributes-from',
+    class_option :"attributes-from",
       banner: "Model",
       type: :string,
       aliases: ["--model", "-m"],
       desc: "Specify to use attributes from a particular model"
 
+    # No short alias: -c is already --omit-comments.
+    class_option :controller,
+      banner: "Name",
+      type: :string,
+      desc: "Generate the controller under this name instead of the resource's, e.g. Api::V1::Posts"
+
     desc "This generator creates a resource file at app/resources, as well as corresponding controller/specs/route/etc"
     def generate_all
       generate_model
@@ -94,7 +106,10 @@ def default_attributes
       end
       if attributes_class.table_exists?
         attributes_class.columns.map do |c|
-          OpenStruct.new({name: c.name.to_sym, type: c.type})
+          OpenStruct.new({
+            name: c.name.to_sym,
+            type: convert_column_type_to_graphiti_resource_type(c.type)
+          })
         end
       else
         raise "#{attributes_class} table must exist. Please run migrations."
@@ -112,12 +127,30 @@ def resource_attributes
       end
     end
 
+    # Generated controllers only use respond_with when it will actually work:
+    # the responders gem being installed is not enough, the controller chain
+    # must include Graphiti::Rails::Responders.
     def responders?
-      defined?(Responders)
+      "ApplicationController".safe_constantize&.include?(Graphiti::Rails::Responders)
+    end
+
+    # Api::V1::Posts, Api::V1::PostsController and api/v1/posts all name the
+    # same controller. Normalizing here means the class name, the file path and
+    # the route all come from one place.
+    def controller_class_name
+      @controller_class_name ||= begin
+        given = options[:controller]
+        base = given ? given.sub(/Controller\z/, "").camelize : model_klass.name
+        "#{base.pluralize}Controller"
+      end
+    end
+
+    def controller_path
+      controller_class_name.sub(/Controller\z/, "").underscore
     end
 
     def generate_controller
-      to = File.join("app/controllers", class_path, "#{file_name.pluralize}_controller.rb")
+      to = File.join("app/controllers", "#{controller_path}_controller.rb")
       template("controller.rb.erb", to)
     end
 
@@ -132,13 +165,14 @@ def application_resource_defined?
     end
 
     def generate_route
-      # Rails 5.2 adds `plural_route_name`, fallback to `plural_table_name`
-      plural_name = try(:plural_route_name) || plural_table_name
-
-      code = "resources :#{plural_name}"
+      code = "resources :#{file_name.pluralize}"
+      # Rails would otherwise infer the controller from the route name, which
+      # is only right when the controller was named after the resource.
+      code << %(, controller: "#{controller_path}") if options[:controller]
       code << %(, only: [#{actions.map { |a| ":#{a}" }.join(", ")}]) if actions.length < 5
       code << "\n"
-      inject_into_file "config/routes.rb", after: /ApplicationResource.*$\n/ do
+      # Matches the install generator's scope, including its pre-2.0 form.
+      inject_into_file "config/routes.rb", after: /format: :jsonapi.*$\n/ do
         indent(code, 4)
       end
     end
@@ -146,12 +180,14 @@ def generate_route
     def generate_resource_specs
       opts = {}
       opts[:actions] = @options[:actions] if @options[:actions]
+      opts[:rawid] = @options[:rawid] if @options[:rawid]
       invoke "graphiti:resource_test", [resource_klass], opts
     end
 
     def generate_api_specs
       opts = {}
       opts[:actions] = @options[:actions] if @options[:actions]
+      opts[:rawid] = @options[:rawid] if @options[:rawid]
       invoke "graphiti:api_test", [resource_klass], opts
     end
 
@@ -176,5 +212,34 @@ def resource_klass
     def type
       model_klass.name.underscore.pluralize
     end
+
+    def convert_column_type_to_graphiti_resource_type(type)
+      # TODO: Support database specific types.
+      case type
+      when :string, :text
+        :string
+      when :float, :decimal
+        :integer
+      when :integer, :bigint
+        :integer
+      when :datetime, :time
+        :datetime
+      when :date
+        :date
+      when :boolean
+        :boolean
+      when :numeric
+        # TODO: Return type.
+        type
+      when :primary_key
+        # TODO: Return type.
+        type
+      when :binary
+        # TODO: Return type.
+        type
+      else
+        type
+      end
+    end
   end
 end
diff --git a/deprecated_generators/graphiti/resource_test_generator.rb b/lib/generators/graphiti/resource_test_generator.rb
similarity index 100%
rename from deprecated_generators/graphiti/resource_test_generator.rb
rename to lib/generators/graphiti/resource_test_generator.rb
diff --git a/lib/generators/graphiti/templates/application_resource.rb.erb b/lib/generators/graphiti/templates/application_resource.rb.erb
new file mode 100644
index 00000000..bfd103bb
--- /dev/null
+++ b/lib/generators/graphiti/templates/application_resource.rb.erb
@@ -0,0 +1,24 @@
+<%- unless omit_comments? -%>
+# All Resources should inherit from ApplicationResource.
+<%- end -%>
+class ApplicationResource < Graphiti::Resource
+  self.abstract_class = true
+  self.adapter = Graphiti::Adapters::ActiveRecord
+  <%- unless omit_comments? -%>
+  # Or follow config/environments:
+  #   self.base_url = ActionDispatch::Http::URL.url_for(Rails.application.routes.default_url_options)
+  <%- end -%>
+  self.base_url = ENV.fetch('BASE_URL', 'http://localhost:3000')
+  self.endpoint_namespace = '<%= api_namespace %>'
+  <%- unless omit_comments? -%>
+
+  # Defaults every Resource inherits. See https://graphiti.dev/concepts/resources
+  <%- resource_setting_groups.each_pair do |group, lines| -%>
+
+  # <%= group.to_s.humanize %>
+    <%- lines.each do |line| -%>
+  # <%= line %>
+    <%- end -%>
+  <%- end -%>
+  <%- end -%>
+end
diff --git a/deprecated_generators/graphiti/templates/controller.rb.erb b/lib/generators/graphiti/templates/controller.rb.erb
similarity index 55%
rename from deprecated_generators/graphiti/templates/controller.rb.erb
rename to lib/generators/graphiti/templates/controller.rb.erb
index 851e8917..b240aeca 100644
--- a/deprecated_generators/graphiti/templates/controller.rb.erb
+++ b/lib/generators/graphiti/templates/controller.rb.erb
@@ -1,25 +1,43 @@
 <% module_namespacing do -%>
-class <%= model_klass.name.pluralize %>Controller < ApplicationController
+class <%= controller_class_name %> < ApplicationController
   <%- if actions?('index') -%>
+  <%- if responders? -%>
   def index
     <%= file_name.pluralize %> = <%= resource_klass %>.all(params)
-    <%- if responders? -%>
     respond_with(<%= file_name.pluralize %>)
-    <%- else -%>
-    render jsonapi: <%= file_name.pluralize %>
-    <%- end -%>
   end
+  <%- else -%>
+  <%- unless omit_comments? -%>
+  # With Graphiti::Rails::Responders included, these blocks collapse to
+  # respond_with(<%= file_name.pluralize %>). See https://graphiti.dev/getting-started/installation#responders
+  <%- end -%>
+  def index
+    <%= file_name.pluralize %> = <%= resource_klass %>.all(params)
+
+    respond_to do |format|
+      format.jsonapi { render jsonapi: <%= file_name.pluralize %> }
+      format.json { render json: <%= file_name.pluralize %> }
+    end
+  end
+  <%- end -%>
   <%- end -%>
   <%- if actions?('show') -%>
 
+  <%- if responders? -%>
   def show
     <%= file_name %> = <%= resource_klass %>.find(params)
-    <%- if responders? -%>
     respond_with(<%= file_name %>)
-    <%- else -%>
-    render jsonapi: <%= file_name %>
-    <%- end -%>
   end
+  <%- else -%>
+  def show
+    <%= file_name %> = <%= resource_klass %>.find(params)
+
+    respond_to do |format|
+      format.jsonapi { render jsonapi: <%= file_name %> }
+      format.json { render json: <%= file_name %> }
+    end
+  end
+  <%- end -%>
   <%- end -%>
   <%- if actions?('create') -%>
 
@@ -38,7 +56,7 @@ class <%= model_klass.name.pluralize %>Controller < ApplicationController
   def update
     <%= file_name %> = <%= resource_klass %>.find(params)
 
-    if <%= file_name %>.update_attributes
+    if <%= file_name %>.update
       render jsonapi: <%= file_name %>
     else
       render jsonapi_errors: <%= file_name %>
diff --git a/deprecated_generators/graphiti/templates/create_request_spec.rb.erb b/lib/generators/graphiti/templates/create_request_spec.rb.erb
similarity index 100%
rename from deprecated_generators/graphiti/templates/create_request_spec.rb.erb
rename to lib/generators/graphiti/templates/create_request_spec.rb.erb
diff --git a/deprecated_generators/graphiti/templates/destroy_request_spec.rb.erb b/lib/generators/graphiti/templates/destroy_request_spec.rb.erb
similarity index 100%
rename from deprecated_generators/graphiti/templates/destroy_request_spec.rb.erb
rename to lib/generators/graphiti/templates/destroy_request_spec.rb.erb
diff --git a/deprecated_generators/graphiti/templates/index_request_spec.rb.erb b/lib/generators/graphiti/templates/index_request_spec.rb.erb
similarity index 73%
rename from deprecated_generators/graphiti/templates/index_request_spec.rb.erb
rename to lib/generators/graphiti/templates/index_request_spec.rb.erb
index df863aea..4d5ea0ab 100644
--- a/deprecated_generators/graphiti/templates/index_request_spec.rb.erb
+++ b/lib/generators/graphiti/templates/index_request_spec.rb.erb
@@ -15,8 +15,8 @@ RSpec.describe "<%= type %>#index", type: :request do
       expect(<%= resource_class %>).to receive(:all).and_call_original
       make_request
       expect(response.status).to eq(200), response.body
-      expect(d.map(&:jsonapi_type).uniq).to match_array(['<%= type %>'])
-      expect(d.map(&:id)).to match_array([<%= var %>1.id, <%= var %>2.id])
+      expect(jsonapi_data.map(&:jsonapi_type).uniq).to match_array(['<%= type %>'])
+      expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to match_array([<%= var %>1.id, <%= var %>2.id])
     end
   end
 end
diff --git a/lib/generators/graphiti/templates/locale.yml.erb b/lib/generators/graphiti/templates/locale.yml.erb
new file mode 100644
index 00000000..1c6b04c7
--- /dev/null
+++ b/lib/generators/graphiti/templates/locale.yml.erb
@@ -0,0 +1,30 @@
+<%- unless omit_comments? -%>
+# For another language, add a block below or a graphiti..yml file.
+<%- end -%>
+en:
+  graphiti:
+    errors:
+<%- unless omit_comments? -%>
+      # Keyed by error code. Edit any value to reword it. Comments name what
+      # raises each code, relative to Graphiti::Errors.
+<%- end -%>
+<%- error_titles.each_pair do |code, title| -%>
+      <%= code %>:
+    <%- unless omit_comments? -%>
+      <%- error_source_lines(code).each do |line| -%>
+        # <%= line %>
+      <%- end -%>
+    <%- end -%>
+        title: <%= title.inspect %>
+    <%- if error_detail(code) -%>
+        detail: <%= error_detail(code).inspect %>
+    <%- end -%>
+<%- end -%>
+<%- unless omit_comments? -%>
+      # format joins the two below: "data.attributes.title is an unknown attribute".
+<%- end -%>
+      format: <%= default_format.inspect %>
+      messages:
+<%- default_messages.each do |code, message| -%>
+        <%= code %>: <%= message.inspect %>
+<%- end -%>
diff --git a/deprecated_generators/graphiti/templates/resource.rb.erb b/lib/generators/graphiti/templates/resource.rb.erb
similarity index 100%
rename from deprecated_generators/graphiti/templates/resource.rb.erb
rename to lib/generators/graphiti/templates/resource.rb.erb
diff --git a/deprecated_generators/graphiti/templates/resource_reads_spec.rb.erb b/lib/generators/graphiti/templates/resource_reads_spec.rb.erb
similarity index 77%
rename from deprecated_generators/graphiti/templates/resource_reads_spec.rb.erb
rename to lib/generators/graphiti/templates/resource_reads_spec.rb.erb
index 7845c432..8ce5f12a 100644
--- a/deprecated_generators/graphiti/templates/resource_reads_spec.rb.erb
+++ b/lib/generators/graphiti/templates/resource_reads_spec.rb.erb
@@ -7,11 +7,11 @@ RSpec.describe <%= resource_class %>, type: :resource do
     it 'works' do
       render
       data = jsonapi_data[0]
-      expect(data.id).to eq(<%= var %>.id)
+      expect(data.<%= id_or_rawid %>).to eq(<%= var %>.id)
       expect(data.jsonapi_type).to eq('<%= type %>')
       <%- attributes.each do |a| -%>
       <%- if [:created_at, :updated_at].include?(a.name.to_sym) -%>
-      expect(data.<%= a.name %>).to eq(datetime(<%= file_name %>.<%= a.name %>))
+      expect(data.<%= a.name %>).to eq(json_datetime(<%= file_name %>.<%= a.name %>))
       <%- else -%>
       expect(data.<%= a.name %>).to eq(<%= file_name %>.<%= a.name %>)
       <%- end -%>
@@ -31,7 +31,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
 
       it 'works' do
         render
-        expect(d.map(&:id)).to eq([<%= var %>2.id])
+        expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to eq([<%= var %>2.id])
       end
     end
   end
@@ -48,10 +48,10 @@ RSpec.describe <%= resource_class %>, type: :resource do
 
         it 'works' do
           render
-          expect(d.map(&:id)).to eq([
+          expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to eq([
             <%= var %>1.id,
             <%= var %>2.id
-          ])
+          ]<%= sort_raw_ids %>)
         end
       end
 
@@ -62,10 +62,10 @@ RSpec.describe <%= resource_class %>, type: :resource do
 
         it 'works' do
           render
-          expect(d.map(&:id)).to eq([
+          expect(jsonapi_data.map(&:<%= id_or_rawid %>)).to eq([
             <%= var %>2.id,
             <%= var %>1.id
-          ])
+          ]<%= sort_raw_ids_descending %>)
         end
       end
     end
diff --git a/deprecated_generators/graphiti/templates/resource_writes_spec.rb.erb b/lib/generators/graphiti/templates/resource_writes_spec.rb.erb
similarity index 96%
rename from deprecated_generators/graphiti/templates/resource_writes_spec.rb.erb
rename to lib/generators/graphiti/templates/resource_writes_spec.rb.erb
index d8ea1b8f..57bda37f 100644
--- a/deprecated_generators/graphiti/templates/resource_writes_spec.rb.erb
+++ b/lib/generators/graphiti/templates/resource_writes_spec.rb.erb
@@ -45,7 +45,7 @@ RSpec.describe <%= resource_class %>, type: :resource do
 
     xit 'works (add some attributes and enable this spec)' do
       expect {
-        expect(instance.update_attributes).to eq(true)
+        expect(instance.update).to eq(true)
       }.to change { <%= var %>.reload.updated_at }
       # .and change { <%= var %>.foo }.to('bar') <- example
     end
diff --git a/deprecated_generators/graphiti/templates/show_request_spec.rb.erb b/lib/generators/graphiti/templates/show_request_spec.rb.erb
similarity index 78%
rename from deprecated_generators/graphiti/templates/show_request_spec.rb.erb
rename to lib/generators/graphiti/templates/show_request_spec.rb.erb
index 0eb83768..cf0d5d24 100644
--- a/deprecated_generators/graphiti/templates/show_request_spec.rb.erb
+++ b/lib/generators/graphiti/templates/show_request_spec.rb.erb
@@ -14,8 +14,8 @@ RSpec.describe "<%= type %>#show", type: :request do
       expect(<%= resource_class %>).to receive(:find).and_call_original
       make_request
       expect(response.status).to eq(200)
-      expect(d.jsonapi_type).to eq('<%= type %>')
-      expect(d.id).to eq(<%= var %>.id)
+      expect(jsonapi_data.jsonapi_type).to eq('<%= type %>')
+      expect(jsonapi_data.<%= id_or_rawid %>).to eq(<%= var %>.id)
     end
   end
 end
diff --git a/deprecated_generators/graphiti/templates/update_request_spec.rb.erb b/lib/generators/graphiti/templates/update_request_spec.rb.erb
similarity index 100%
rename from deprecated_generators/graphiti/templates/update_request_spec.rb.erb
rename to lib/generators/graphiti/templates/update_request_spec.rb.erb
diff --git a/lib/graphiti-rails.rb b/lib/graphiti-rails.rb
new file mode 100644
index 00000000..ce5cbb52
--- /dev/null
+++ b/lib/graphiti-rails.rb
@@ -0,0 +1,11 @@
+# Deprecated entry point. The graphiti-rails gem was absorbed into graphiti in
+# 2.0, and its integration loads automatically whenever Rails is defined, so
+# this require is no longer needed. It is kept so an explicit
+# `require "graphiti-rails"` left over from 1.x does not raise. Remove in 3.0.
+require "graphiti"
+require "graphiti/rails"
+
+Graphiti::DEPRECATOR.warn(
+  'require "graphiti-rails" is no longer needed. graphiti-rails is part of graphiti as of 2.0, ' \
+  "and the Rails integration is loaded for you when Rails is present."
+)
diff --git a/lib/graphiti.rb b/lib/graphiti.rb
index 6802f53b..09bcfd57 100644
--- a/lib/graphiti.rb
+++ b/lib/graphiti.rb
@@ -3,6 +3,12 @@
 require "uri"
 require "ostruct" unless defined?(::OpenStruct)
 
+# The umbrella require, not just the core_ext files below: Graphiti.broadcast
+# calls ActiveSupport::Notifications.instrument, which reaches for
+# ActiveSupport::IsolatedExecutionState. That constant is autoloaded here and
+# nowhere else, so without this a non-Rails app raises NameError on the first
+# render unless something else happened to load ActiveSupport for us.
+require "active_support"
 require "active_support/version"
 require "active_support/deprecation"
 require "active_support/deprecator" if ::ActiveSupport.version >= Gem::Version.new("7.1")
@@ -14,27 +20,51 @@
 require "active_support/time"
 
 require "dry-types"
-require "graphiti_errors"
 
 require "jsonapi/serializable"
 
+# These gems are merged into graphiti as of 2.0, but their 1.x/0.x releases
+# still resolve against graphiti 2.x. Leaving one installed is never harmless:
+# graphiti_spec_helpers and graphiti-rails ship files that collide with ours
+# (lib/graphiti_spec_helpers.rb, lib/graphiti/rails.rb), so which copy a require
+# picks up comes down to load path order, and graphiti_errors installs a second,
+# competing exception handler on any controller that includes it. Fail loudly
+# rather than let either happen quietly. Checked here because graphiti is loaded
+# either way, whichever copy wins.
+{
+  "graphiti_spec_helpers" => 'The "graphiti_spec_helpers/rspec" require and the GraphitiSpecHelpers namespace are unchanged.',
+  "graphiti-rails" => 'Graphiti::Rails and its config.graphiti options are unchanged. Drop the "graphiti-rails" require if you have one, and add `include Graphiti::Rails::Controller` to controllers serving Graphiti resources. graphiti-rails installed that on every controller automatically. See graphiti.dev/upgrading.',
+  "graphiti_errors" => "Exception handling now goes through rescue_registry. Remove `include GraphitiErrors` from your controllers — Graphiti registers its own handlers, and you can add yours with `register_exception`."
+}.each do |gem_name, guidance|
+  next unless Gem.loaded_specs.key?(gem_name)
+
+  raise <<~MSG
+    #{gem_name} is merged into graphiti as of 2.0 and is no longer published
+    separately. Remove it from your Gemfile.
+
+    #{guidance}
+  MSG
+end
+
 module Graphiti
-  DEPRECATOR = ActiveSupport::Deprecation.new("2.0", "Graphiti")
+  DEPRECATOR = ActiveSupport::Deprecation.new("3.0", "Graphiti")
 
   # @api private
   def self.context
-    Thread.current[:context] ||= {}
+    Fiber[:context] ||= ContextHash.new
   end
 
   # @api private
   def self.context=(val)
-    Thread.current[:context] = val
+    Fiber[:context] = ContextHash.new.tap do |hash|
+      val.each { |key, value| hash[key] = value }
+    end
   end
 
   # @api private
-  def self.with_context(obj, namespace = nil)
+  def self.with_context(obj, action = nil)
     prior = context
-    self.context = {object: obj, namespace: namespace}
+    self.context = {object: obj, action: action}
     yield
   ensure
     self.context = prior
@@ -121,6 +151,11 @@ def self.setup!
     resources.each do |r|
       r.apply_sideloads_to_serializer
     end
+    @setup = true
+  end
+
+  def self.setup?
+    !!@setup
   end
 
   def self.cache=(val)
@@ -136,10 +171,14 @@ def self.cache
 require "graphiti/jsonapi_serializable_ext"
 require "graphiti/configuration"
 require "graphiti/context"
+require "graphiti/context_hash"
 require "graphiti/errors"
 require "graphiti/types"
+require "graphiti/audit"
+require "graphiti/audit/report"
 require "graphiti/schema"
 require "graphiti/schema_diff"
+require "graphiti/schema/check"
 require "graphiti/adapters/abstract"
 require "graphiti/resource/sideloading"
 require "graphiti/resource/links"
@@ -203,6 +242,11 @@ def self.cache
 require "graphiti/debugger"
 require "graphiti/util/cache_debug"
 require "graphiti/util/uri_decoder"
+require "graphiti/error_serializers/translated_title"
+require "graphiti/error_serializers/validation"
+require "graphiti/error_serializers/invalid_request"
+require "graphiti/error_serializers/conflict_request"
+require "graphiti/error_serializers/deprecated_constants"
 
 if defined?(ActiveRecord)
   require "graphiti/adapters/active_record"
@@ -210,14 +254,6 @@ def self.cache
 
 if defined?(Rails)
   require "graphiti/rails"
-  require "graphiti/responders"
-
-  # graphiti-rails has own Railtie
-  begin
-    require "graphiti-rails"
-  rescue LoadError
-    require "graphiti/railtie"
-  end
 end
 
 require "graphiti/runner"
diff --git a/lib/graphiti/adapters/abstract.rb b/lib/graphiti/adapters/abstract.rb
index 8b3dfb96..165682c5 100644
--- a/lib/graphiti/adapters/abstract.rb
+++ b/lib/graphiti/adapters/abstract.rb
@@ -371,10 +371,11 @@ def associate(parent, child, association_name, association_type)
           activerecord_adapter.associate \
             parent, child, association_name, association_type
         elsif [:has_many, :many_to_many].include?(association_type)
-          if parent.send(:"#{association_name}").nil?
+          associated = parent.send(:"#{association_name}")
+          if associated.nil?
             parent.send(:"#{association_name}=", [child])
-          else
-            parent.send(:"#{association_name}") << child
+          elsif associated.none? { |existing| existing.equal?(child) }
+            associated << child
           end
         else
           parent.send(:"#{association_name}=", child)
diff --git a/lib/graphiti/adapters/active_record.rb b/lib/graphiti/adapters/active_record.rb
index ae48b401..92532733 100644
--- a/lib/graphiti/adapters/active_record.rb
+++ b/lib/graphiti/adapters/active_record.rb
@@ -246,7 +246,7 @@ def associate_all(parent, children, association_name, association_type)
 
           children.each do |child|
             if association_type == :many_to_many &&
-                [:create, :update].include?(Graphiti.context[:namespace]) &&
+                [:create, :update].include?(Graphiti.context[:action]) &&
                 !parent.send(association_name).exists?(child.id) &&
                 child.errors.blank?
               parent.send(association_name) << child
@@ -281,20 +281,6 @@ def disassociate(parent, child, association_name, association_type)
         # Nothing to do in the else case, happened when we merged foreign key
       end
 
-      # (see Adapters::Abstract#create)
-      def create(model_class, create_params)
-        instance = model_class.new(create_params)
-        instance.save
-        instance
-      end
-
-      # (see Adapters::Abstract#update)
-      def update(model_class, update_params)
-        instance = model_class.find(update_params.only(:id))
-        instance.update_attributes(update_params.except(:id))
-        instance
-      end
-
       def save(model_instance)
         model_instance.save
         model_instance
diff --git a/lib/graphiti/audit.rb b/lib/graphiti/audit.rb
new file mode 100644
index 00000000..8f8dd378
--- /dev/null
+++ b/lib/graphiti/audit.rb
@@ -0,0 +1,230 @@
+module Graphiti
+  # Resources register themselves as they load, so eager load before auditing.
+  class Audit
+    Finding = Struct.new(:severity, :check, :message, :remedy) do
+      def error?
+        severity == :error
+      end
+    end
+
+    Row = Struct.new(
+      :resource,
+      :relationship,
+      :type,
+      :target,
+      :resource_ids_source,
+      :resource_ids_source_if_always,
+      :resource_ids_default,
+      :resource_ids_blocker,
+      :preloaded,
+      :options,
+      :findings
+    ) do
+      def would_start_loading?
+        return false if resource_ids_default == :never
+
+        resource_ids_source != :load && resource_ids_source_if_always == :load
+      end
+
+      def severity
+        return :error if findings.any?(&:error?)
+        findings.empty? ? :ok : :warning
+      end
+
+      def error?
+        severity == :error
+      end
+    end
+
+    def self.run(resources = nil)
+      new(resources).run
+    end
+
+    def self.findings(resources = nil)
+      run(resources).flat_map(&:findings)
+    end
+
+    def initialize(resources = nil)
+      @resources = resources || Graphiti.resources.reject(&:abstract_class?)
+    end
+
+    def run
+      @resources.sort_by { |resource| resource.name.to_s }.flat_map { |resource| rows_for(resource) }
+    end
+
+    private
+
+    def rows_for(resource_class)
+      model = inferred_model(resource_class)
+
+      resource_class.sideloads.map do |name, sideload|
+        row_for(resource_class, name, sideload, model)
+      end
+    end
+
+    def row_for(resource_class, name, sideload, model)
+      source = resource_ids_source(sideload)
+
+      Row.new(
+        resource: resource_class.name,
+        relationship: name,
+        type: sideload.type,
+        target: target_name(sideload),
+        resource_ids_source: source,
+        resource_ids_source_if_always: resource_ids_source_if_always(resource_class, sideload),
+        resource_ids_default: resource_class.belongs_to_resource_ids_by_default,
+        resource_ids_blocker: sideload.resource_ids_blocker,
+        preloaded: (source == :load) ? association_preloaded?(resource_class, sideload) : nil,
+        options: declaration_options(sideload),
+        findings: [
+          missing_association_method(sideload, model),
+          missing_guard_method(resource_class, sideload),
+          missing_sideload_filter(sideload)
+        ].compact
+      )
+    rescue => error
+      Row.new(
+        resource: resource_class.name,
+        relationship: name,
+        type: sideload.type,
+        resource_ids_source: :none,
+        options: {},
+        findings: [
+          Finding.new(
+            severity: :error,
+            check: :broken_relationship,
+            message: "#{error.class}: #{error.message.strip.lines.first.to_s.strip}"
+          )
+        ]
+      )
+    end
+
+    def declaration_options(sideload)
+      options = sideload.non_default_options
+      options[:base_scope] = "..." if sideload.customized_base_scope?
+      options[:scope] = "..." if sideload.class.scope_proc
+      options[:params] = "..." if sideload.class.params_proc
+      options
+    end
+
+    def resource_ids_source(sideload)
+      return :none unless sideload.render_resource_ids?
+
+      sideload.resource_ids_blocker.nil? ? :key : :load
+    end
+
+    def resource_ids_source_if_always(resource_class, sideload)
+      original = resource_class.belongs_to_resource_ids_by_default
+      resource_class.belongs_to_resource_ids_by_default = :always
+      resource_ids_source(sideload)
+    ensure
+      resource_class.belongs_to_resource_ids_by_default = original
+    end
+
+    def target_name(sideload)
+      if sideload.type == :polymorphic_belongs_to
+        children = sideload.children.values.filter_map { |child| short_name(child) }
+        return children.empty? ? nil : children.join(", ")
+      end
+
+      sideload.resource.class.name
+    rescue
+      nil
+    end
+
+    def short_name(sideload)
+      sideload.resource.class.name&.split("::")&.last
+    rescue
+      nil
+    end
+
+    def inferred_model(resource_class)
+      model = resource_class.model
+      model.is_a?(Class) ? model : nil
+    rescue Errors::ModelNotFound
+      nil
+    end
+
+    def missing_association_method(sideload, model)
+      return unless model
+      return if sideload.type == :polymorphic_belongs_to
+      return if model.method_defined?(sideload.association_name)
+      return if model.private_method_defined?(sideload.association_name)
+
+      Finding.new(
+        severity: :error,
+        check: :missing_association_method,
+        message: "#{model.name} has no ##{sideload.association_name} method",
+        remedy: "define it, point the relationship at the real association with `as:`, or remove the relationship"
+      )
+    end
+
+    def missing_guard_method(resource_class, sideload)
+      guard = sideload.readable_guard_name
+      return unless guard
+      return if defines?(resource_class, guard)
+      return if defines?(sideload.resource.class, guard)
+
+      Finding.new(
+        severity: :error,
+        check: :missing_guard_method,
+        message: "##{guard} is defined on neither #{resource_class.name} nor its related resource",
+        remedy: "define the guard on either resource"
+      )
+    end
+
+    def defines?(resource_class, method_name)
+      resource_class.method_defined?(method_name) ||
+        resource_class.private_method_defined?(method_name)
+    end
+
+    def missing_sideload_filter(sideload)
+      return if builds_its_own_query?(sideload)
+
+      key = sideload_filter_key(sideload)
+      return unless key
+
+      related = sideload.resource.class
+      return if related.filters.key?(key)
+
+      Finding.new(
+        severity: :error,
+        check: :missing_sideload_filter,
+        message: "#{related.name} is missing `filter #{key.inspect}`",
+        remedy: "declare the filter on the related resource"
+      )
+    end
+
+    def builds_its_own_query?(sideload)
+      sideload.class.params_proc || sideload.class.scope_proc
+    end
+
+    def sideload_filter_key(sideload)
+      case sideload.type
+      when :has_many, :has_one then sideload.foreign_key
+      when :many_to_many then sideload.true_foreign_key
+      when :belongs_to then sideload.primary_key
+      end
+    end
+
+    def association_preloaded?(resource_class, sideload)
+      scope = resource_class.new.base_scope
+      return nil unless scope.respond_to?(:includes_values)
+
+      preloads = [scope.includes_values, scope.preload_values, scope.eager_load_values]
+      association_names(preloads).include?(sideload.association_name.to_sym)
+    rescue
+      nil
+    end
+
+    def association_names(preload_values)
+      Array(preload_values).flat_map do |value|
+        case value
+        when Hash then value.keys.map(&:to_sym)
+        when Array then association_names(value)
+        else [value.to_sym]
+        end
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/audit/report.rb b/lib/graphiti/audit/report.rb
new file mode 100644
index 00000000..0de48ca4
--- /dev/null
+++ b/lib/graphiti/audit/report.rb
@@ -0,0 +1,226 @@
+module Graphiti
+  class Audit
+    class Report
+      ANSI = {red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, bold: 1, dim: 2}.freeze
+
+      ISSUE_HEADINGS = {
+        broken_relationship: "raised while being inspected",
+        missing_association_method: "will raise when the relationship is included: the model has no association method",
+        missing_guard_method: "will raise whenever the resource renders: the readable guard is not defined",
+        missing_sideload_filter: "will raise when the relationship is included: the related resource is missing the filter"
+      }.freeze
+
+      CHECKLIST = {
+        broken_relationship: ["all relationships inspectable", "relationship", "raised while being inspected"],
+        missing_association_method: ["all association methods defined", "association method", "missing"],
+        missing_guard_method: ["all readable guards defined", "readable guard", "missing"],
+        missing_sideload_filter: ["all sideload filters declared", "sideload filter", "missing"],
+        loads_on_every_render: ["all id-rendering loads preloaded", "relationship", "loading ids without preloading"]
+      }.freeze
+
+      Cell = Struct.new(:raw, :painted) do
+        def pad_to(width)
+          painted + " " * (width - raw.length)
+        end
+      end
+
+      def initialize(rows, color: $stdout.tty?)
+        @rows = rows
+        @color = color
+      end
+
+      def to_s
+        return "graphiti: no relationships found.\n" if @rows.empty?
+
+        (body + [summary]).join("\n")
+      end
+
+      private
+
+      def body
+        [""] + issue_sections + loading_section + would_load_section + checks_section
+      end
+
+      def issue_sections
+        row_finding_pairs = @rows.flat_map do |row|
+          row.findings.map { |finding| [row, finding] }
+        end
+
+        row_finding_pairs
+          .group_by { |_, finding| finding.check }
+          .sort_by { |check, _| ISSUE_HEADINGS.keys.index(check) || ISSUE_HEADINGS.size }
+          .flat_map { |check, group| issue_section(check, group) }
+      end
+
+      def issue_section(check, group)
+        findings = group.map(&:last)
+        label = findings.first.error? ? "ERROR" : "WARNING"
+        color = findings.first.error? ? :red : :yellow
+        fixes = findings.filter_map(&:remedy).uniq
+
+        section_heading(label, color, ISSUE_HEADINGS.fetch(check, check.to_s.tr("_", " "))) +
+          lines_grouped_by_resource(group.map { |row, finding| [row, finding.message] }) +
+          ((fixes.size == 1) ? [fix_line(fixes.first), ""] : [])
+      end
+
+      def loading_section
+        rows = @rows.select { |row| loads_without_preload?(row) }
+        return [] if rows.empty?
+
+        section_heading("WARNING", :yellow, "rendering resource ids by loading an association the base_scope does not preload") +
+          lines_grouped_by_resource(rows.map { |row| [row, nil] }) +
+          ["  #{paint("That is a query per record rendered.", :dim)}",
+            fix_line("preload the association in base_scope, or drop `resource_ids`"),
+            "  #{paint("See graphiti.dev/concepts/relationships#customizing-relationships", :dim)}",
+            ""]
+      end
+
+      def loads_without_preload?(row)
+        row.resource_ids_source == :load && row.preloaded == false
+      end
+
+      def would_load_section
+        rows = @rows.select(&:would_start_loading?)
+        return [] if rows.empty?
+
+        verb = (rows.size == 1) ? "renders" : "render"
+
+        section_heading("FYI", :blue, "#{count(rows.size, "belongs_to relationship")} #{verb} no resource ids") +
+          lines_grouped_by_resource(rows.map { |row| [row, nil] }) +
+          ["  #{paint("Nothing to fix. A request that includes the relationship still gets its ids.", :dim)}",
+            "",
+            "  #{paint("To render ids on every response, opt in:", :dim)}",
+            ""] +
+          opt_in_lines +
+          ["",
+            "  #{paint("Either would load the association on every render. That potential performance cost is why ids are opt-in.", :dim)}",
+            "  #{paint("Preloading the association in base_scope keeps that load cheap.", :dim)}",
+            "  #{paint("See graphiti.dev/concepts/relationships#belongs-to-resource-ids", :dim)}",
+            ""]
+      end
+
+      def checks_section
+        failure_counts = @rows.flat_map(&:findings).map(&:check).tally
+        loading = @rows.count { |row| loads_without_preload?(row) }
+        failure_counts[:loads_on_every_render] = loading if loading > 0
+
+        no_loads = @rows.none? { |row| row.resource_ids_source == :load }
+
+        lines = CHECKLIST.filter_map do |check, (passed, failure_noun, failure_suffix)|
+          if (failures = failure_counts[check])
+            "  #{paint("✗", :red)} #{count(failures, failure_noun)} #{failure_suffix}"
+          elsif check == :loads_on_every_render && no_loads
+            nil
+          else
+            "  #{paint("✓", :green)} #{passed}"
+          end
+        end
+
+        [paint("checks", :bold), ""] + lines + [""]
+      end
+
+      def section_heading(label, color, heading)
+        ["#{paint(label, color, :bold)} #{paint(heading, :bold)}", ""]
+      end
+
+      def fix_line(text)
+        "  #{paint("fix:", :green)} #{text}"
+      end
+
+      def lines_grouped_by_resource(pairs)
+        width = pairs.map { |row, _| declaration_cell(row).raw.length }.max
+
+        pairs.group_by { |row, _| row.resource }.sort_by { |resource, _| resource }.flat_map do |resource, group|
+          ["  #{paint(resource, :bold)}"] +
+            group.sort_by { |row, _| row.relationship.to_s }.map { |row, note|
+              declaration = declaration_cell(row)
+              note ? "    #{declaration.pad_to(width)}  #{note}" : "    #{declaration.painted}"
+            } +
+            [""]
+        end
+      end
+
+      def opt_in_lines
+        options = [
+          [[["resource_ids: ", :cyan], ["true", :yellow]], "on one relationship"],
+          [[["self.belongs_to_resource_ids_by_default = ", nil], [":always", :cyan]], "across the whole API"]
+        ]
+
+        rows = options.map do |parts, scope|
+          [cell(parts), cell([[scope, :dim]])]
+        end
+
+        aligned(rows, "    ")
+      end
+
+      def declaration_cell(row)
+        cell(declaration_parts(row))
+      end
+
+      def declaration_parts(row)
+        parts = [[row.type.to_s, :blue], [" ", nil], [":#{row.relationship}", :cyan]]
+
+        row.options.each do |key, value|
+          parts << [", ", nil] << ["#{key}: ", :cyan] << option_part(value)
+        end
+
+        parts
+      end
+
+      def option_part(value)
+        case value
+        when "..." then ["{ ... }", :dim]
+        when true, false then [value.inspect, :yellow]
+        when Symbol then [value.inspect, :cyan]
+        when String then [value.inspect, :green]
+        else [value.inspect, nil]
+        end
+      end
+
+      def cell(parts)
+        Cell.new(
+          parts.map { |text, _| text }.join,
+          parts.map { |text, style| paint(text, style) }.join
+        )
+      end
+
+      def aligned(rows, indent)
+        widths = rows.transpose.map { |column| column.map { |cell| cell.raw.length }.max }
+
+        rows.map do |cells|
+          line = cells.each_with_index.map { |cell, index|
+            (index == cells.size - 1) ? cell.painted : cell.pad_to(widths[index])
+          }.join("  ")
+          indent + line
+        end
+      end
+
+      def summary
+        errors = @rows.count(&:error?)
+        loading = @rows.count { |row| loads_without_preload?(row) }
+        no_ids = @rows.count(&:would_start_loading?)
+
+        counts = [
+          count(@rows.group_by(&:resource).size, "resource"),
+          count(@rows.size, "relationship"),
+          paint(count(errors, "error"), (errors > 0) ? :red : nil)
+        ]
+        counts << paint("#{loading} loading ids without preloading", :yellow) if loading > 0
+        counts << "#{no_ids} without resource ids" if no_ids > 0
+
+        "graphiti: #{counts.join(", ")}."
+      end
+
+      def count(number, noun)
+        "#{number} #{noun}#{"s" unless number == 1}"
+      end
+
+      def paint(text, *styles)
+        styles = styles.compact
+        return text unless @color && styles.any?
+
+        "\e[#{styles.map { |style| ANSI.fetch(style) }.join(";")}m#{text}\e[0m"
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/configuration.rb b/lib/graphiti/configuration.rb
index 2792eeb1..cfe0922b 100644
--- a/lib/graphiti/configuration.rb
+++ b/lib/graphiti/configuration.rb
@@ -24,10 +24,6 @@ class Configuration
 
     attr_accessor :respond_to
     attr_accessor :context_for_endpoint
-    attr_accessor :links_on_demand
-    attr_accessor :pagination_links_on_demand
-    attr_accessor :pagination_links
-    attr_accessor :typecast_reads
     attr_accessor :raise_on_missing_sidepost
     attr_accessor :before_sideload
 
@@ -44,32 +40,12 @@ def initialize
       @concurrency = false
       @concurrency_max_threads = 4
       @respond_to = [:json, :jsonapi, :xml]
-      @links_on_demand = false
-      @pagination_links_on_demand = false
-      @pagination_links = false
-      @typecast_reads = true
       @raise_on_missing_sidepost = true
       @cache_rendering = false
       self.debug = ENV.fetch("GRAPHITI_DEBUG", true)
       self.debug_models = ENV.fetch("GRAPHITI_DEBUG_MODELS", false)
 
       @uri_decoder = infer_uri_decoder
-
-      # FIXME: Don't duplicate graphiti-rails efforts
-      if defined?(::Rails.root) && (root = ::Rails.root)
-        config_file = root.join(".graphiticfg.yml")
-        if config_file.exist?
-          cfg = YAML.load_file(config_file)
-          @schema_path = root.join("public#{cfg["namespace"]}/schema.json")
-        else
-          @schema_path = root.join("public/schema.json")
-        end
-
-        if (logger = ::Rails.logger)
-          self.debug = logger.debug? && debug
-          Graphiti.logger = logger
-        end
-      end
     end
 
     def cache_rendering?
@@ -87,13 +63,20 @@ def schema_path
     end
 
     def debug=(val)
-      @debug = val
-      Debugger.enabled = val
+      @debug = coerce_flag(val)
+      Debugger.enabled = @debug
     end
 
     def debug_models=(val)
-      @debug_models = val
-      Debugger.debug_models = val
+      @debug_models = coerce_flag(val)
+      Debugger.debug_models = @debug_models
+    end
+
+    # every value comes back from ENV as a string and "false" is truthy
+    def coerce_flag(val)
+      return false if ["false", "0", ""].include?(val.to_s.strip.downcase)
+
+      !!val
     end
 
     def with_option(key, value)
@@ -104,6 +87,48 @@ def with_option(key, value)
       send(:"#{key}=", original)
     end
 
+    def typecast_reads
+      Resource.typecast_reads
+    end
+
+    def typecast_reads=(val)
+      Resource.typecast_reads = val
+    end
+
+    def links_on_demand
+      Resource.relationship_links == :on_demand
+    end
+
+    def links_on_demand=(val)
+      if val
+        Resource.relationship_links = :on_demand
+      elsif Resource.relationship_links == :on_demand
+        Resource.relationship_links = true
+      end
+    end
+
+    def pagination_links
+      Resource.page_links == true
+    end
+
+    def pagination_links=(val)
+      return if Resource.page_links == :on_demand
+
+      Resource.page_links = !!val
+    end
+
+    def pagination_links_on_demand
+      Resource.page_links == :on_demand
+    end
+
+    def pagination_links_on_demand=(val)
+      if val
+        Resource.page_links = :on_demand
+      elsif Resource.page_links == :on_demand
+        Resource.page_links = false
+      end
+    end
+
     def uri_decoder=(decoder)
       unless decoder.respond_to?(:call)
         raise "uri_decoder must respond to `call`."
@@ -132,6 +157,19 @@ def infer_uri_decoder
     end
   end
 
-  msg = "Use graphiti-rails's `config.graphiti.respond_to_formats`"
+  msg = "Use `config.graphiti.respond_to_formats`"
   DEPRECATOR.deprecate_methods(Configuration, respond_to: msg, "respond_to=": msg)
+
+  relationship_msg = "Set `self.relationship_links` (true, false, or :on_demand) on your resource"
+  pagination_msg = "Set `self.page_links` (true, false, or :on_demand) on your resource"
+  typecast_msg = "Set `self.typecast_reads` on your resource"
+  DEPRECATOR.deprecate_methods(Configuration,
+    typecast_reads: typecast_msg,
+    "typecast_reads=": typecast_msg,
+    links_on_demand: relationship_msg,
+    "links_on_demand=": relationship_msg,
+    pagination_links: pagination_msg,
+    "pagination_links=": pagination_msg,
+    pagination_links_on_demand: pagination_msg,
+    "pagination_links_on_demand=": pagination_msg)
 end
diff --git a/lib/graphiti/context_hash.rb b/lib/graphiti/context_hash.rb
new file mode 100644
index 00000000..dfe70826
--- /dev/null
+++ b/lib/graphiti/context_hash.rb
@@ -0,0 +1,21 @@
+module Graphiti
+  # graphiti-api/graphiti#134 recommended reading Graphiti.context[:namespace] from base_scope before current_action existed, so the old key keeps working for one major.
+  class ContextHash < Hash
+    def [](key)
+      super(deprecate_namespace(key))
+    end
+
+    def []=(key, value)
+      super(deprecate_namespace(key), value)
+    end
+
+    private
+
+    def deprecate_namespace(key)
+      return key unless key == :namespace
+
+      Graphiti::DEPRECATOR.deprecation_warning(:"context[:namespace]", "Use #current_action instead", caller_locations(2))
+      :action
+    end
+  end
+end
diff --git a/lib/graphiti/debugger.rb b/lib/graphiti/debugger.rb
index b800498a..40d11771 100644
--- a/lib/graphiti/debugger.rb
+++ b/lib/graphiti/debugger.rb
@@ -11,13 +11,13 @@ class << self
     private_constant :CHUNKS
 
     class << self
-      # Pool threads inherit this through the thread-local copy Scope#future_with_context makes, so their chunks reach the right request.
+      # Pool threads inherit this through the fiber storage copy Scope#future_with_context makes, so their chunks reach the right request.
       def chunks
-        Thread.current[CHUNKS] ||= Concurrent::Array.new
+        Fiber[CHUNKS] ||= Concurrent::Array.new
       end
 
       def chunks=(value)
-        Thread.current[CHUNKS] = value
+        Fiber[CHUNKS] = value
       end
 
       def on_data(name, start, stop, id, payload)
diff --git a/lib/graphiti/delegates/pagination.rb b/lib/graphiti/delegates/pagination.rb
index 4fb2dfee..b3d9ac8c 100644
--- a/lib/graphiti/delegates/pagination.rb
+++ b/lib/graphiti/delegates/pagination.rb
@@ -6,7 +6,7 @@ def initialize(proxy)
       end
 
       def links?
-        @proxy.query.pagination_links? && @proxy.data.present?
+        @proxy.query.page_links? && @proxy.data.present?
       end
 
       def links
@@ -32,7 +32,7 @@ def has_previous_page?
       private
 
       def pagination_params
-        @pagination_params ||= @proxy.query.params.reject { |key, _| [:action, :controller, :format].include?(key) }
+        @pagination_params ||= @proxy.query.params.except(:action, :controller, :format)
       end
 
       def pagination_link(page)
@@ -104,12 +104,12 @@ def offset
 
       def page_size
         @page_size ||= (page_param[:size] ||
-                        @proxy.resource.default_page_size ||
+                        @proxy.resource.page_default_size ||
                         Graphiti::Scoping::Paginate::DEFAULT_PAGE_SIZE).to_i
       end
 
       def page_param
-        @page_param ||= (@proxy.query.hash[:page] || {})
+        @page_param ||= @proxy.query.hash[:page] || {}
       end
     end
   end
diff --git a/lib/graphiti/deprecated_tasks.rb b/lib/graphiti/deprecated_tasks.rb
deleted file mode 100644
index 8efcfab8..00000000
--- a/lib/graphiti/deprecated_tasks.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-namespace :graphiti do
-  def session
-    @session ||= ActionDispatch::Integration::Session.new(Rails.application)
-  end
-
-  def setup_rails!
-    Rails.application.eager_load!
-    Rails.application.config.cache_classes = true
-    Rails.application.config.action_controller.perform_caching = false
-  end
-
-  def make_request(path, debug = false)
-    if path.split("/").length == 2
-      path = "#{ApplicationResource.endpoint_namespace}#{path}"
-    end
-    path << if path.include?("?")
-      "&cache=bust"
-    else
-      "?cache=bust"
-    end
-    path = "#{path}&debug=true" if debug
-    session.get(path.to_s)
-    JSON.parse(session.response.body)
-  end
-
-  desc "Execute request without web server."
-  task :request, [:path, :debug] => [:environment] do |_, args|
-    setup_rails!
-    Graphiti.logger = Graphiti.stdout_logger
-    Graphiti::Debugger.preserve = true
-    require "pp"
-    path, debug = args[:path], args[:debug]
-    puts "Graphiti Request: #{path}"
-    json = make_request(path, debug)
-    pp json
-    Graphiti::Debugger.flush if debug
-  end
-
-  desc "Execute benchmark without web server."
-  task :benchmark, [:path, :requests] => [:environment] do |_, args|
-    setup_rails!
-    took = Benchmark.ms {
-      args[:requests].to_i.times do
-        make_request(args[:path])
-      end
-    }
-    puts "Took: #{(took / args[:requests].to_f).round(2)}ms"
-  end
-end
diff --git a/lib/graphiti/deserializer.rb b/lib/graphiti/deserializer.rb
index dc7ce8c2..7c38002b 100644
--- a/lib/graphiti/deserializer.rb
+++ b/lib/graphiti/deserializer.rb
@@ -85,7 +85,7 @@ def attributes
   def meta(action: :update)
     {
       type: data[:type],
-      temp_id: data[:'temp-id'],
+      temp_id: data[:"temp-id"],
       method: action,
       payload_path: ["data"]
     }
@@ -162,12 +162,12 @@ def process_relationship(relationship_data)
   end
 
   def process_relationship_datum(datum)
-    temp_id = datum[:'temp-id']
+    temp_id = datum[:"temp-id"]
     included_object = included.find { |i|
       next unless i[:type] == datum[:type]
 
       (i[:id] && i[:id] == datum[:id]) ||
-        (i[:'temp-id'] && i[:'temp-id'] == temp_id)
+        (i[:"temp-id"] && i[:"temp-id"] == temp_id)
     }
     included_idx = included.index(included_object)
 
diff --git a/lib/graphiti/error_serializers/conflict_request.rb b/lib/graphiti/error_serializers/conflict_request.rb
new file mode 100644
index 00000000..3714f48f
--- /dev/null
+++ b/lib/graphiti/error_serializers/conflict_request.rb
@@ -0,0 +1,19 @@
+module Graphiti
+  module ErrorSerializers
+    # graphiti_errors overrode only the status here, and so reported a 409
+    # alongside code "bad_request".
+    class ConflictRequest < InvalidRequest
+      STATUS = 409
+
+      private
+
+      def code
+        "conflict"
+      end
+
+      def default_title
+        "Conflict Error"
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/error_serializers/deprecated_constants.rb b/lib/graphiti/error_serializers/deprecated_constants.rb
new file mode 100644
index 00000000..c541b02d
--- /dev/null
+++ b/lib/graphiti/error_serializers/deprecated_constants.rb
@@ -0,0 +1,61 @@
+# Only graphiti_errors' serializers survived the 2.0 merge. Remove in 3.0.
+module GraphitiErrors
+  # Including an empty module succeeds, so silence here means HTML error pages.
+  def self.included(klass)
+    raise <<~MSG
+      GraphitiErrors merged into graphiti in 2.0 and this include does nothing. Use:
+
+        include Graphiti::Rails::Controller
+
+      Your own exceptions register alongside Graphiti's with `register_exception`.
+      `registered_exception?` is now `RescueRegistry.handles_exception?`, and
+      `handle_exception` went with the rendering. See graphiti.dev/upgrading.
+    MSG
+  end
+
+  # The old global rendering toggle, most often flipped in specs.
+  class << self
+    def enable!
+      Graphiti::DEPRECATOR.deprecation_warning("GraphitiErrors.enable!", "wrap the request in Graphiti::Rails::TestHelpers#handle_request_exceptions instead")
+      test_helpers.handle_request_exceptions(true)
+    end
+
+    def disable!
+      Graphiti::DEPRECATOR.deprecation_warning("GraphitiErrors.disable!", "wrap the request in Graphiti::Rails::TestHelpers#handle_request_exceptions instead")
+      test_helpers.handle_request_exceptions(false)
+    end
+
+    def disabled?
+      !test_helpers.handle_request_exceptions?
+    end
+
+    private
+
+    def test_helpers
+      @test_helpers ||= Object.new.extend(Graphiti::Rails::TestHelpers)
+    end
+  end
+
+  module Validation; end
+
+  module InvalidRequest; end
+
+  module ConflictRequest; end
+
+  module Serializers; end
+end
+
+{
+  "GraphitiErrors::Validation::Serializer" => "Graphiti::ErrorSerializers::Validation",
+  "GraphitiErrors::InvalidRequest::Serializer" => "Graphiti::ErrorSerializers::InvalidRequest",
+  "GraphitiErrors::ConflictRequest::Serializer" => "Graphiti::ErrorSerializers::ConflictRequest",
+  # Graphiti 1.0.x referenced this spelling
+  "GraphitiErrors::Serializers::Validation" => "Graphiti::ErrorSerializers::Validation"
+}.each do |old_name, new_name|
+  namespace, _, constant = old_name.rpartition("::")
+
+  Object.const_get(namespace).const_set(
+    constant,
+    ActiveSupport::Deprecation::DeprecatedConstantProxy.new(old_name, new_name, Graphiti::DEPRECATOR)
+  )
+end
diff --git a/lib/graphiti/error_serializers/invalid_request.rb b/lib/graphiti/error_serializers/invalid_request.rb
new file mode 100644
index 00000000..25a0522c
--- /dev/null
+++ b/lib/graphiti/error_serializers/invalid_request.rb
@@ -0,0 +1,64 @@
+module Graphiti
+  module ErrorSerializers
+    class InvalidRequest
+      include TranslatedTitle
+
+      STATUS = 400
+
+      attr_reader :source
+
+      def initialize(source)
+        @source = source
+      end
+
+      def status
+        self.class::STATUS
+      end
+
+      def errors
+        [].tap do |payload|
+          source.details.each_pair do |attribute, attribute_errors|
+            attribute_errors.each_with_index do |error, index|
+              message = source.messages[attribute][index]
+
+              payload << {
+                code: code,
+                status: status.to_s,
+                title: title,
+                detail: source.full_message(attribute, message),
+                source: {pointer: pointer_for(attribute)},
+                meta: {
+                  attribute: attribute,
+                  message: message,
+                  code: error[:error]
+                }
+              }
+            end
+          end
+        end
+      end
+
+      # graphiti_errors named the payload this. Remove in 3.0.
+      def rendered_errors
+        Graphiti::DEPRECATOR.deprecation_warning("#rendered_errors", "use #errors")
+        errors
+      end
+
+      private
+
+      def code
+        "bad_request"
+      end
+
+      def default_title
+        "Request Error"
+      end
+
+      # "filter.foo[0]" describes a path into the payload, not an attribute
+      # name, so it becomes a JSON pointer.
+      def pointer_for(attribute)
+        attribute.to_s.tr(".", "/").gsub(/\[(\d+)\]/, '/\1')
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/error_serializers/translated_title.rb b/lib/graphiti/error_serializers/translated_title.rb
new file mode 100644
index 00000000..1e1e7689
--- /dev/null
+++ b/lib/graphiti/error_serializers/translated_title.rb
@@ -0,0 +1,13 @@
+module Graphiti
+  module ErrorSerializers
+    module TranslatedTitle
+      private
+
+      def title
+        return default_title unless defined?(::I18n)
+
+        ::I18n.t :title, scope: [:graphiti, :errors, code], default: default_title
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/error_serializers/validation.rb b/lib/graphiti/error_serializers/validation.rb
new file mode 100644
index 00000000..1d71697b
--- /dev/null
+++ b/lib/graphiti/error_serializers/validation.rb
@@ -0,0 +1,153 @@
+module Graphiti
+  module ErrorSerializers
+    # Walks into related objects so nested writes report against the record
+    # that actually failed.
+    class Validation
+      include TranslatedTitle
+
+      attr_reader :object
+
+      def initialize(object, relationship_payloads = {}, relationship_meta = {})
+        @object = object
+        @relationship_payloads = relationship_payloads
+        @relationship_meta = relationship_meta
+      end
+
+      def attribute_errors
+        [].tap do |errors|
+          each_error do |attribute, message, validation_code|
+            errors << {
+              code: code,
+              status: "422",
+              title: title,
+              detail: detail_for(attribute, message),
+              source: {pointer: pointer_for(object, attribute)},
+              meta: meta_for(attribute, message, validation_code, @relationship_meta)
+            }
+          end
+        end
+      end
+
+      def errors
+        return [] unless object.respond_to?(:errors)
+
+        attribute_errors | relationship_errors(object, @relationship_payloads)
+      end
+
+      private
+
+      def code
+        "unprocessable_entity"
+      end
+
+      def default_title
+        "Validation Error"
+      end
+
+      def each_error
+        object.errors.messages.each_pair do |attribute, messages|
+          details = object.errors.details.find { |k, _| k == attribute }[1]
+
+          messages.each_with_index do |message, index|
+            yield attribute, message, details[index][:error]
+          end
+        end
+      end
+
+      def relationship?(name)
+        relationship_names = []
+        if activerecord?
+          relationship_names = object.class
+            .reflect_on_all_associations.map(&:name)
+        elsif object.respond_to?(:relationship_names)
+          relationship_names = object.relationship_names
+        end
+
+        relationship_names.include?(name)
+      end
+
+      def attribute?(name)
+        object.respond_to?(name)
+      end
+
+      def meta_for(attribute, message, code, relationship_meta)
+        meta = {
+          attribute: attribute,
+          message: message,
+          code: code
+        }
+
+        unless relationship_meta.empty?
+          meta = {relationship: meta.merge(relationship_meta)}
+        end
+
+        meta
+      end
+
+      def detail_for(attribute, message)
+        detail = object.errors.full_message(attribute, message)
+        detail = message if attribute.to_s.downcase == "base"
+        detail
+      end
+
+      # @richmolj: Keeping this to support ember-data, but I hate the concept.
+      def pointer_for(object, name)
+        if relationship?(name)
+          "/data/relationships/#{name}"
+        elsif attribute?(name)
+          "/data/attributes/#{name}"
+        elsif name == :base
+          nil
+        else
+          # Probably a nested relation, like post.comments
+          "/data/relationships/#{name}"
+        end
+      end
+
+      def activerecord?
+        object.class.respond_to?(:reflect_on_all_associations)
+      end
+
+      def traverse_relationships(model, relationship_params)
+        return unless relationship_params
+
+        relationship_params.each_pair do |name, payload|
+          relationship_objects = Array(model.send(name))
+
+          relationship_objects.each do |relationship_object|
+            related_payload = payload
+            if payload.is_a?(Array)
+              related_payload = payload.find { |p|
+                temp_id = relationship_object
+                  .instance_variable_get(:@_jsonapi_temp_id)
+                p[:meta][:temp_id] === temp_id ||
+                  p[:meta][:id] == relationship_object.id.to_s
+              }
+            end
+
+            yield name, relationship_object, related_payload
+            relationship_errors(relationship_object, related_payload[:relationships])
+          end
+        end
+      end
+
+      def relationship_errors(model, relationship_payloads)
+        errors = []
+        traverse_relationships(model, relationship_payloads) do |name, related, payload|
+          meta = {}.tap do |hash|
+            hash[:name] = name
+            hash[:type] = payload[:meta][:jsonapi_type]
+            if (temp_id = related.instance_variable_get(:@_jsonapi_temp_id))
+              hash[:"temp-id"] = temp_id
+            else
+              hash[:id] = related.id
+            end
+          end
+
+          errors |= self.class.new(related, payload[:relationships], meta).errors
+        end
+        errors
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/errors.rb b/lib/graphiti/errors.rb
index bae2fd50..6d54cc4e 100644
--- a/lib/graphiti/errors.rb
+++ b/lib/graphiti/errors.rb
@@ -221,7 +221,7 @@ def message
 
           Make sure the endpoint "#{@sideload.resource.endpoint[:full_path]}" exists with action #{@action.inspect}, or customize the endpoint for #{@sideload.resource.class.name}.
 
-          If you do not wish to generate a link, pass link: false or set self.autolink = false.
+          If you do not wish to generate a link, pass link: false or set self.relationship_links = false. To keep the link and stop checking it, set self.validate_links = false.
         MSG
       end
     end
@@ -255,6 +255,8 @@ def message
           Graphiti.config.context_for_endpoint must be set to enable link generation:
 
           Graphiti.config.context_for_endpoint = ->(path, action) { ... }
+
+          Or set self.validate_links = false to render links without checking them.
         MSG
       end
     end
@@ -270,7 +272,7 @@ def message
           #{@resource_class.name}: error occurred while sideloading "#{@sideload_name}"!
 
           The error was raised while attempting to build query parameters for the associated Resource.
-          Read more about sideload scoping here: www.graphiti.dev/guides/concepts/resources#customizing-scope
+          Read more about sideload scoping here: graphiti.dev/concepts/relationships#customizing-scope
 
           A good way to debug is to put a debugger within the 'params' block.
 
@@ -294,7 +296,7 @@ def message
 
           The error was raised while attempting to build the scope for the associated Resource.
 
-          Read more about sideload scoping here: www.graphiti.dev/guides/concepts/resources#customizing-scope
+          Read more about sideload scoping here: graphiti.dev/concepts/relationships#customizing-scope
 
           Here's the original, underlying error:
 
@@ -315,7 +317,7 @@ def message
           #{@resource_class.name}: error occurred while sideloading "#{@sideload_name}"!
 
           The error was raised while attempting to assign relevant model instances. Read
-          more about sideload assignment here: www.graphiti.dev/guides/concepts/resources#customizing-assignment
+          more about sideload assignment here: graphiti.dev/concepts/relationships#customizing-assignment
 
           A good way to debug is to put a debugger within the 'assign' block.
 
@@ -445,11 +447,11 @@ def message
 
           secondary_endpoint '/my_url', [:index, :update]
 
-          Or disable endpoint validation for this resource:
+          Or disable request validation for this resource:
 
-          self.validate_endpoints = false
+          self.validate_requests = false
 
-          See https://www.graphiti.dev/guides/concepts/links for more information.
+          See https://graphiti.dev/concepts/links for more information.
 
           The current endpoints allowed for this resource are: #{@resource_class.endpoints.inspect}
         MSG
@@ -689,6 +691,99 @@ def message
       end
     end
 
+    class InvalidBelongsToResourceIds < Base
+      def initialize(resource_class, value)
+        @resource_class = resource_class
+        @value = value
+      end
+
+      def message
+        <<~MSG
+          #{@resource_class.name}: belongs_to_resource_ids_by_default must be one of :foreign_key, :always, or :never. Got #{@value.inspect}.
+
+            :foreign_key - render resource ids wherever the foreign key already holds the related id (default)
+            :always      - render them for every belongs_to, loading the association when the foreign key cannot answer
+            :never       - render none
+
+          To render resource ids for a collection, ask for it one relationship at a time with `resource_ids: true`.
+        MSG
+      end
+    end
+
+    class InvalidFilterBlanks < Base
+      def initialize(resource_class, attribute, value)
+        @resource_class = resource_class
+        @attribute = attribute
+        @value = value
+      end
+
+      def message
+        <<~MSG
+          #{@resource_class.name}: #{@attribute} must be one of :literal, :null, or :rejected. Got #{@value.inspect}.
+
+            :literal  - "null" and "" reach the filter as strings (default)
+            :null     - "null" becomes nil, so the filter can query for NULL
+            :rejected - a blank value raises InvalidFilterValue
+        MSG
+      end
+    end
+
+    class InvalidLinkRendering < Base
+      def initialize(resource_class, attribute, value)
+        @resource_class = resource_class
+        @attribute = attribute
+        @value = value
+      end
+
+      def message
+        "#{@resource_class.name}: #{@attribute} must be true, false, or :on_demand. Got #{@value.inspect}."
+      end
+    end
+
+    class UnselectedForeignKey < Base
+      def initialize(resource_class, sideload, model)
+        @resource_class = resource_class
+        @sideload = sideload
+        @model = model
+      end
+
+      def message
+        <<~MSG
+          #{@resource_class.name}: rendering resource linkage for relationship #{@sideload.name.inspect} reads ##{@sideload.foreign_key} off #{@model.class.name}, but that attribute is not loaded.
+
+          Keep #{@sideload.foreign_key} in the selected columns, or turn linkage off with `resource_ids: false` on the relationship or `self.belongs_to_resource_ids_by_default = :never` on the resource.
+        MSG
+      end
+    end
+
+    class MissingRelationshipMethod < Base
+      def initialize(resource_class, sideload, model)
+        @resource_class = resource_class
+        @sideload = sideload
+        @model = model
+      end
+
+      def message
+        <<~MSG
+          #{@resource_class.name}: relationship #{@sideload.name.inspect} is declared, but #{@model.class.name} has no ##{@sideload.association_name} method.
+
+          Rendering the relationship reads the association off the model. Define ##{@sideload.association_name} on #{@model.class.name}, point the relationship at the real association with `as:`, or remove the relationship.
+          #{resource_ids_note}
+        MSG
+      end
+
+      private
+
+      def resource_ids_note
+        return "" unless @sideload.render_resource_ids?
+
+        <<~MSG
+
+          resource_ids is set on this relationship, so every render reads the association, not just requests that include it. See graphiti.dev/concepts/relationships#customizing-relationships.
+        MSG
+      end
+    end
+
     class MissingDependentFilter < Base
       def initialize(resource, filters)
         @resource = resource
@@ -862,7 +957,7 @@ class FilterGroupMissingRequiredFilters < Base
       def initialize(resource, filter_names, required)
         @resource = resource
         @filter_names = filter_names
-        @required_label = required == :all ? "All" : "One"
+        @required_label = (required == :all) ? "All" : "One"
       end
 
       def message
diff --git a/lib/graphiti/extensions/temp_id.rb b/lib/graphiti/extensions/temp_id.rb
index 39f003ae..b39d7985 100644
--- a/lib/graphiti/extensions/temp_id.rb
+++ b/lib/graphiti/extensions/temp_id.rb
@@ -14,7 +14,7 @@ module SerializableTempId
     def as_jsonapi(*)
       super.tap do |hash|
         if (temp_id = @object.instance_variable_get(:@_jsonapi_temp_id))
-          hash[:'temp-id'] = temp_id
+          hash[:"temp-id"] = temp_id
         end
       end
     end
diff --git a/lib/graphiti/query.rb b/lib/graphiti/query.rb
index ba1916c0..0fbe00c8 100644
--- a/lib/graphiti/query.rb
+++ b/lib/graphiti/query.rb
@@ -2,9 +2,20 @@
 
 module Graphiti
   class Query
+    TRUTHY_LINKS = [true, "true"].freeze
+    LINKLESS_FORMATS = [:json, :xml, "json", "xml"].freeze
+
     attr_reader :resource, :association_name, :params, :action
 
-    def initialize(resource, params, association_name = nil, nested_include = nil, parents = [], action = nil)
+    def initialize(resource, params, *positional, association_name: nil, nested_include: nil, parents: [], action: nil)
+      if positional.any?
+        Graphiti::DEPRECATOR.warn("Passing Query.new trailing arguments positionally is deprecated. Use association_name:/nested_include:/parents:/action: keywords.")
+        association_name ||= positional[0]
+        nested_include ||= positional[1]
+        parents = positional[2] if positional.length > 2
+        action ||= positional[3]
+      end
+
       @resource = resource
       @association_name = association_name
       @params = params
@@ -14,35 +25,91 @@ def initialize(resource, params, association_name = nil, nested_include = nil, p
       @include_param = nested_include || @params[:include]
       @parents = parents
       @action = parse_action(action)
+      @entity_map = Concurrent::Map.new if parents.empty?
+      @association_owners = Concurrent::Map.new if parents.empty?
     end
 
     def association?
       !!@association_name
     end
 
+    # Concurrent::Map because sideload scopes resolve on pool threads
+    def entity_map
+      return root.entity_map unless root == self
+
+      @entity_map
+    end
+
+    def association_owners
+      return root.association_owners unless root == self
+
+      @association_owners
+    end
+
+    # Deduplication exists for a row two include paths both resolve, so a
+    # resource class sitting at one node has nothing to collide with.
+    def repeated_resource_classes
+      return root.repeated_resource_classes unless root == self
+
+      @repeated_resource_classes ||= begin
+        counts = Hash.new(0)
+        walk = [self]
+        while (query = walk.pop)
+          counts[query.resource.class] += 1
+          query.sideloads.each_pair do |name, sideload_query|
+            walk << sideload_query
+            sideload = query.resource.class.sideload(name)
+            next unless sideload.respond_to?(:children)
+
+            # A polymorphic child resolves under a class no node names.
+            sideload.children.each_value { |child| counts[child.resource.class] += 2 }
+          end
+        end
+        counts.select { |_, count| count > 1 }.keys.to_set
+      end
+    end
+
     def top_level?
       !association?
     end
 
-    def links?
-      return false if [:json, :xml, "json", "xml"].include?(params[:format])
-      if Graphiti.config.links_on_demand
-        [true, "true"].include?(@params[:links])
-      else
-        true
-      end
+    def includes_requested?
+      @include_param.present?
+    end
+
+    def links_requested?
+      return @links_requested if defined?(@links_requested)
+
+      @links_requested = TRUTHY_LINKS.include?(@params[:links])
+    end
+
+    def suppress_links?
+      return @suppress_links if defined?(@suppress_links)
+
+      @suppress_links = LINKLESS_FORMATS.include?(params[:format])
     end
 
-    def pagination_links?
+    def render_link?(mode)
+      return false if suppress_links?
+
+      (mode == :on_demand) ? links_requested? : mode == true
+    end
+
+    def page_links?
       if action == :find
         false
-      elsif Graphiti.config.pagination_links_on_demand
-        [true, "true"].include?(@params[:pagination_links])
+      elsif @resource.page_links == :on_demand
+        page_links_requested?
       else
-        Graphiti.config.pagination_links
+        @resource.page_links
       end
     end
 
+    def page_links_requested?
+      [@params[:page_links], @params[:pagination_links]]
+        .any? { |value| [true, "true"].include?(value) }
+    end
+
     def debug_requested?
       !!@params[:debug]
     end
@@ -109,9 +176,10 @@ def sideloads
             relationship_name = sideload ? sideload.name : key
             hash[relationship_name] = Query.new sl_resource,
               @params,
-              key,
-              sub_hash,
-              query_parents, :all
+              association_name: key,
+              nested_include: sub_hash,
+              parents: query_parents,
+              action: :all
           else
             handle_missing_sideload(key)
           end
@@ -123,6 +191,10 @@ def parents
       @parents ||= []
     end
 
+    def root
+      parents.first || self
+    end
+
     def fields
       @fields ||= begin
         hash = parse_fieldset(@params[:fields] || {})
@@ -208,14 +280,14 @@ def include_hash
         allowlist = nil
         if @resource.context&.respond_to?(:sideload_allowlist)
           allowlist = @resource.context.sideload_allowlist
-          allowlist = allowlist[@resource.context_namespace] if allowlist
+          allowlist = allowlist[@resource.current_action] if allowlist
         end
 
         scrubbed = allowlist ? Util::IncludeParams.scrub(requested, allowlist) : requested
 
         scrubbed.filter do |key, value|
           sideload = @resource.class.sideload(key)
-          sideload.nil? ? true : sideload.readable?
+          sideload.nil? || sideload.readable?
         end
       end
     end
@@ -246,8 +318,8 @@ def cache_key
     def query_cache_key
       attrs = {extra_fields: extra_fields,
                fields: fields,
-               links: links?,
-               pagination_links: pagination_links?,
+               links: suppress_links? ? :none : links_requested?,
+               page_links: page_links?,
                format: params[:format]}
 
       Digest::SHA1.hexdigest(attrs.to_s)
@@ -291,7 +363,7 @@ def nested?(name)
       return false unless association?
 
       split = name.to_s.split(".")
-      query_names = split[0..split.length - 2].map(&:to_sym)
+      query_names = split[0..-2].map(&:to_sym)
       my_names = parents.map(&:association_name).compact + [association_name].compact
       query_names == my_names
     end
@@ -323,7 +395,7 @@ def handle_missing_sideload(name)
     end
 
     def sort_hash(attr)
-      value = attr[0] == "-" ? :desc : :asc
+      value = (attr[0] == "-") ? :desc : :asc
       key = attr.sub("-", "").to_sym
 
       {key => value}
@@ -356,7 +428,7 @@ def sort_hashes
     end
 
     def parse_action(action)
-      action ||= @params.fetch(:action, Graphiti.context[:namespace]).try(:to_sym)
+      action ||= @params.fetch(:action, Graphiti.context[:action]).try(:to_sym)
       case action
       when :index
         :all
@@ -366,5 +438,9 @@ def parse_action(action)
         action
       end
     end
+
+    alias_method :pagination_links?, :page_links?
+    DEPRECATOR.deprecate_methods(self,
+      pagination_links?: "Use `page_links?`")
   end
 end
diff --git a/lib/graphiti/rails.rb b/lib/graphiti/rails.rb
index cf5028eb..1d477730 100644
--- a/lib/graphiti/rails.rb
+++ b/lib/graphiti/rails.rb
@@ -1,38 +1,88 @@
+require "rescue_registry"
+require "graphiti"
+require "rails"
+
 module Graphiti
-  # Rails Integration. Mix this in to ApplicationController.
-  #
-  # * Mixes in Base
-  # * Adds a global around_action (see Base#wrap_context)
-  #
-  # @see Base#render_jsonapi
-  # @see Base#wrap_context
+  # Rails integration for Graphiti. See {file:README.md} for more details.
   module Rails
+    # Graphiti::Rails was a controller mixin before it became this namespace.
+    # Including a namespace is not an error, it just does nothing, so honour
+    # the old spelling rather than leave a controller silently without its
+    # context, debugger and exception handlers.
     def self.included(klass)
-      backtrace = ::Rails::VERSION::MAJOR == 4 ? caller(2) : caller_locations(2)
-      Graphiti::DEPRECATOR.deprecation_warning("Including Graphiti::Rails", "Use graphiti-rails instead. See https://www.graphiti.dev/guides/graphiti-rails-migration for details.", backtrace)
-
-      klass.class_eval do
-        include Graphiti::Context
-        include GraphitiErrors
-        around_action :wrap_context
-        around_action :debug
-      end
-    end
+      DEPRECATOR.deprecation_warning(
+        "Including Graphiti::Rails",
+        "include Graphiti::Rails::Controller instead"
+      )
 
-    def wrap_context
-      Graphiti.with_context(jsonapi_context, action_name.to_sym) do
-        yield
-      end
+      klass.include(Controller)
     end
 
-    def debug
-      Debugger.debug do
-        yield
-      end
-    end
+    # graphiti-rails had its own, and apps that silenced it by name still resolve.
+    DEPRECATOR = Graphiti::DEPRECATOR
 
-    def jsonapi_context
-      self
-    end
+    autoload :ConflictRequestHandler, "graphiti/rails/exception_handlers"
+    autoload :Context, "graphiti/rails/context"
+    autoload :Controller, "graphiti/rails/controller"
+    autoload :Debugging, "graphiti/rails/debugging"
+    autoload :ExceptionHandler, "graphiti/rails/exception_handlers"
+    autoload :FallbackHandler, "graphiti/rails/exception_handlers"
+    autoload :InvalidRequestHandler, "graphiti/rails/exception_handlers"
+    autoload :Responders, "graphiti/rails/responders"
+    autoload :TestHelpers, "graphiti/rails/test_helpers"
+
+    # Deprecated. Backed graphiti-rails' enable!/disable! spec toggles. Remove in 3.0.
+    GraphitiErrorsTesting = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
+      "Graphiti::Rails::GraphitiErrorsTesting",
+      "Graphiti::Rails::TestHelpers",
+      DEPRECATOR
+    )
+
+    # Rails reports an exception to the error reporter unless its rescue_responses names it.
+    CLIENT_ERROR_STATUSES = {
+      "Graphiti::Errors::InvalidRequest" => :bad_request,
+      "Graphiti::Errors::RemoteWrite" => :bad_request,
+      "Graphiti::Errors::SingularSideload" => :bad_request,
+      "Graphiti::Errors::InvalidInclude" => :bad_request,
+      "Graphiti::Errors::UnsupportedSort" => :bad_request,
+      "Graphiti::Errors::UnsupportedOperator" => :bad_request,
+      "Graphiti::Errors::InvalidFilterValue" => :bad_request,
+      "Graphiti::Errors::RequiredFilter" => :bad_request,
+      "Graphiti::Errors::UnsupportedPageSize" => :bad_request,
+      "Graphiti::Errors::UnsupportedBeforeCursor" => :bad_request,
+      "Graphiti::Errors::UnsupportedPagination" => :bad_request,
+      "Graphiti::Errors::FilterGroupMissingRequiredFilters" => :bad_request,
+      "Graphiti::Errors::RecordNotFound" => :not_found,
+      "Graphiti::Errors::ConflictRequest" => :conflict
+    }.freeze
+
+    # @!attribute self.handled_exception_formats
+    # A list of formats as symbols whose exceptions will be handled by Graphiti. See {Railtie}.
+    cattr_accessor :handled_exception_formats, default: []
+
+    # @!attribute self.respond_to_formats
+    # A list of formats as symbols which will be available for Graphiti::Rails::Responders. See {Railtie}.
+    cattr_accessor :respond_to_formats, default: []
   end
+
+  # Deprecated. Was core's own responders mixin, superseded by the one that came
+  # in with graphiti-rails. Remove in 3.0.
+  Responders = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
+    "Graphiti::Responders",
+    "Graphiti::Rails::Responders",
+    DEPRECATOR
+  )
+
+  # Deprecated. Core's railtie merged into graphiti-rails'. Remove in 3.0.
+  Railtie = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
+    "Graphiti::Railtie",
+    "Graphiti::Rails::Railtie",
+    DEPRECATOR
+  )
 end
+
+ActiveSupport.on_load(:active_record) do
+  require "graphiti/adapters/active_record"
+end
+
+require "graphiti/rails/railtie"
diff --git a/lib/graphiti/rails/context.rb b/lib/graphiti/rails/context.rb
new file mode 100644
index 00000000..ebba38b2
--- /dev/null
+++ b/lib/graphiti/rails/context.rb
@@ -0,0 +1,34 @@
+module Graphiti
+  module Rails
+    # Wraps controller actions in a [Graphiti Context](https://graphiti.dev/concepts/resources#context) which points to the
+    # controller instance by default.
+    module Context
+      def self.included(klass)
+        klass.class_eval do
+          include Graphiti::Context
+
+          around_action :wrap_graphiti_context
+        end
+      end
+
+      # Called by [`#around_action`](https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-around_action)
+      # to wrap the current action in a Graphiti context defined by {#graphiti_context}.
+      def wrap_graphiti_context
+        Graphiti.with_context(graphiti_context, action_name.to_sym) do
+          yield
+        end
+      end
+
+      # The context to use for Graphiti Resources. Defaults to the controller instance.
+      # Can be redefined for different behavior.
+      def graphiti_context
+        if respond_to?(:jsonapi_context)
+          DEPRECATOR.deprecation_warning("Overriding jsonapi_context", "Override #graphiti_context instead")
+          jsonapi_context
+        else
+          self
+        end
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/rails/controller.rb b/lib/graphiti/rails/controller.rb
new file mode 100644
index 00000000..45b2ad96
--- /dev/null
+++ b/lib/graphiti/rails/controller.rb
@@ -0,0 +1,64 @@
+module Graphiti
+  module Rails
+    # Everything a controller needs to serve Graphiti resources. Include it in
+    # the controller your API inherits from:
+    #
+    #   class ApplicationController < ActionController::Base
+    #     include Graphiti::Rails::Controller
+    #   end
+    #
+    # Graphiti applied all of this to every controller in the application until
+    # 2.0, which meant wrapping unrelated actions in a Graphiti context and
+    # registering a catch-all exception handler on controllers that never touch
+    # Graphiti. Scoping it is now up to you.
+    module Controller
+      extend ActiveSupport::Concern
+
+      included do
+        include Graphiti::Rails::Context
+        include Graphiti::Rails::Debugging
+        # ActionController::API strips this out, but Graphiti serves multiple
+        # formats (jsonapi/json/xml), so respond_to has to work everywhere.
+        include ActionController::MimeResponds
+
+        # Broad, but the handler only renders formats listed in
+        # config.graphiti.handled_exception_formats and hands everything else
+        # back to Rails.
+        register_exception Exception,
+          status: :passthrough,
+          handler: Graphiti::Rails::FallbackHandler
+
+        register_exception Graphiti::Errors::InvalidRequest,
+          status: 400, handler: Graphiti::Rails::InvalidRequestHandler
+        register_exception Graphiti::Errors::RemoteWrite,
+          status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+        register_exception Graphiti::Errors::SingularSideload,
+          status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+        register_exception Graphiti::Errors::InvalidInclude,
+          status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+        register_exception Graphiti::Errors::UnsupportedSort,
+          status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+        register_exception Graphiti::Errors::UnsupportedOperator,
+          status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+        register_exception Graphiti::Errors::InvalidFilterValue,
+          status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+        register_exception Graphiti::Errors::RequiredFilter,
+          status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+        register_exception Graphiti::Errors::UnsupportedPageSize,
+          status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+        register_exception Graphiti::Errors::UnsupportedBeforeCursor,
+          status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+        register_exception Graphiti::Errors::UnsupportedPagination,
+          status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+        register_exception Graphiti::Errors::FilterGroupMissingRequiredFilters,
+          status: 400, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+
+        register_exception Graphiti::Errors::RecordNotFound,
+          status: 404, detail: :exception, handler: Graphiti::Rails::ExceptionHandler
+
+        register_exception Graphiti::Errors::ConflictRequest,
+          status: 409, handler: Graphiti::Rails::ConflictRequestHandler
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/rails/debugging.rb b/lib/graphiti/rails/debugging.rb
new file mode 100644
index 00000000..693e2656
--- /dev/null
+++ b/lib/graphiti/rails/debugging.rb
@@ -0,0 +1,18 @@
+module Graphiti
+  module Rails
+    # Wraps controller actions in a [Graphiti Debugger](https://graphiti.dev/topics/debugging#debugger).
+    module Debugging
+      def self.included(klass)
+        klass.around_action :debug_graphiti
+      end
+
+      # Called by [`#around_action`](https://api.rubyonrails.org/classes/AbstractController/Callbacks/ClassMethods.html#method-i-around_action)
+      # to wrap the current action in a Graphiti Debugger.
+      def debug_graphiti
+        Debugger.debug do
+          yield
+        end
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/rails/exception_handlers.rb b/lib/graphiti/rails/exception_handlers.rb
new file mode 100644
index 00000000..e4660215
--- /dev/null
+++ b/lib/graphiti/rails/exception_handlers.rb
@@ -0,0 +1,94 @@
+module Graphiti
+  module Rails
+    class ExceptionHandler < RescueRegistry::ExceptionHandler
+      # We've actually changed the signature here which is somewhat risky...
+      def build_payload(show_details: false, traces: nil, style: :rails)
+        case style
+        when :standard
+          super(show_details: show_details, traces: traces).tap do |payload|
+            if show_details
+              # For Vandal and Request Responses
+              payload[:__raw_error__] = {
+                message: exception.message,
+                debug: exception.instance_variable_get(:@__graphiti_debug),
+                backtrace: exception.backtrace
+              }
+            end
+          end
+        when :rails
+          # TODO: Find way to not duplicate RailsExceptionHandler
+          body = {
+            status: status_code,
+            error: title
+          }
+
+          if show_details
+            body[:exception] = exception.inspect
+            body[:traces] = traces if traces
+          end
+
+          body
+        else
+          raise ArgumentError, "unknown style #{style}"
+        end
+      end
+
+      def formatted_response(content_type, **options)
+        # We're relying on the fact that `formatted_response` passes through unknown options to `build_payload`
+        if Graphiti::Rails.handled_exception_formats.include?(content_type.to_sym)
+          options[:style] = :standard
+        end
+        super
+      end
+
+      private
+
+      def title_from_status
+        translate(:title) || super
+      end
+
+      # rescue_registry's default tells the client its engineers have been notified.
+      def default_detail_for_status
+        translate(:detail)
+      end
+
+      def translate(key)
+        return unless defined?(::I18n)
+
+        ::I18n.t key, scope: [:graphiti, :errors, error_code], default: nil
+      end
+    end
+
+    class FallbackHandler < ExceptionHandler
+      # A nil response is what hands the exception back to the default Rails
+      # handler, so formats Graphiti does not claim render as they always did.
+      def formatted_response(content_type, **options)
+        return unless Graphiti::Rails.handled_exception_formats.include?(content_type.to_sym)
+
+        super
+      end
+    end
+
+    class InvalidRequestHandler < ExceptionHandler
+      # NOTE: `style` is ignored. A rejected request always reports which
+      # parameters were rejected and why.
+      def build_payload(show_details: false, traces: nil, style: nil)
+        {errors: error_serializer.new(exception.errors).errors}
+      end
+
+      private
+
+      def error_serializer
+        Graphiti::ErrorSerializers::InvalidRequest
+      end
+    end
+
+    class ConflictRequestHandler < InvalidRequestHandler
+      private
+
+      def error_serializer
+        Graphiti::ErrorSerializers::ConflictRequest
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/rails/railtie.rb b/lib/graphiti/rails/railtie.rb
new file mode 100644
index 00000000..f7592c19
--- /dev/null
+++ b/lib/graphiti/rails/railtie.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+module Graphiti
+  module Rails
+    # This Railtie exposes some configuration options:
+    # * `config.graphiti.handled_exception_formats`, defaulting to `[:jsonapi]`.
+    #   Formats in this list will always have their exceptions handled by Graphiti.
+    # * `config.graphiti.respond_to_formats`, defaulting to `[:json, :jsonapi, :xml]`.
+    #   When {Graphiti::Rails::Responders} is included in a controller it will respond
+    #   to these mime types by default.
+    class Railtie < ::Rails::Railtie
+      config.graphiti = ActiveSupport::OrderedOptions.new
+
+      config.graphiti.handled_exception_formats = [:jsonapi]
+
+      old_respond_to_formats = Graphiti::DEPRECATOR.silence { Graphiti.config.try(:respond_to) }
+      config.graphiti.respond_to_formats = old_respond_to_formats || [:json, :jsonapi, :xml]
+
+      rake_tasks do
+        load File.expand_path("../../tasks/graphiti.rake", __dir__)
+      end
+
+      # ActionDispatch has already copied config.action_dispatch.rescue_responses into the wrapper by now.
+      initializer "graphiti-rails.rescue_responses", after: "action_dispatch.configure" do
+        responses = ActionDispatch::ExceptionWrapper.rescue_responses
+
+        Graphiti::Rails::CLIENT_ERROR_STATUSES.each do |exception, status|
+          # rescue_responses answers :internal_server_error for a missing key, so ||= never assigns.
+          responses[exception] = status unless responses.key?(exception)
+        end
+      end
+
+      initializer "graphiti-rails.config" do |app|
+        Graphiti::Rails.handled_exception_formats = app.config.graphiti.handled_exception_formats
+        Graphiti::Rails.respond_to_formats = app.config.graphiti.respond_to_formats
+
+        root = ::Rails.root
+
+        config_file = root.join(".graphiticfg.yml")
+        if config_file.exist?
+          cfg = YAML.load_file(config_file)
+          Graphiti.config.schema_path = root.join("public#{cfg["namespace"]}/schema.json")
+        else
+          Graphiti.config.schema_path = root.join("public/schema.json")
+        end
+      end
+
+      initializer "graphti-rails.logger" do
+        config.after_initialize do
+          Graphiti.config.debug = ::Rails.logger.debug? && Graphiti.config.debug
+          Graphiti.logger = ::Rails.logger
+        end
+      end
+
+      initializer "graphiti-rails.init" do
+        if ::Rails.application.config.eager_load
+          config.after_initialize do |app|
+            ::Rails.application.reload_routes!
+            Graphiti.setup!
+          end
+        end
+
+        register_mime_type
+        register_parameter_parser
+        register_renderers
+        establish_concurrency
+        configure_endpoint_lookup
+      end
+
+      # from jsonapi-rails
+      MEDIA_TYPE = "application/vnd.api+json"
+
+      PARSER = lambda do |body|
+        data = JSON.parse(body)
+        data[:format] = :jsonapi
+        data.with_indifferent_access
+      end
+
+      def register_mime_type
+        Mime::Type.register(MEDIA_TYPE, :jsonapi) if Mime[:jsonapi].nil?
+      end
+
+      def register_parameter_parser
+        ActionDispatch::Request.parameter_parsers[:jsonapi] ||= PARSER
+      end
+
+      def register_renderers
+        ActiveSupport.on_load(:action_controller) do
+          ::ActionController::Renderers.add(:jsonapi) do |proxy, options|
+            self.content_type ||= Mime[:jsonapi]
+
+            # opts = {}
+            # if respond_to?(:default_jsonapi_render_options)
+            #   opts = default_jsonapi_render_options
+            # end
+
+            if proxy.is_a?(Hash) # for destroy
+              render(options.merge(json: proxy))
+            else
+              proxy.to_jsonapi(options)
+            end
+          end
+        end
+
+        ActiveSupport.on_load(:action_controller) do
+          ActionController::Renderers.add(:jsonapi_errors) do |proxy, options|
+            self.content_type ||= Mime[:jsonapi]
+
+            validation = Graphiti::ErrorSerializers::Validation.new \
+              proxy.data, proxy.payload.relationships
+
+            render \
+              json: {errors: validation.errors},
+              status: :unprocessable_entity
+          end
+        end
+      end
+
+      # Only run concurrently if our environment supports it
+      def establish_concurrency
+        Graphiti.config.concurrency = !::Rails.env.test? &&
+          ::Rails.application.config.cache_classes
+      end
+
+      def configure_endpoint_lookup
+        Graphiti.config.context_for_endpoint = ->(path, action) {
+          method = :GET
+          case action
+          when :show then path = "#{path}/1"
+          when :create then method = :POST
+          when :update
+            path = "#{path}/1"
+            method = :PUT
+          when :destroy
+            path = "#{path}/1"
+            method = :DELETE
+          end
+
+          route = begin
+            ::Rails.application.routes.recognize_path(path.to_s, method: method)
+          rescue
+            nil
+          end
+          "#{route[:controller]}_controller".classify.safe_constantize if route
+        }
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/rails/rake_helpers.rb b/lib/graphiti/rails/rake_helpers.rb
new file mode 100644
index 00000000..586d52f2
--- /dev/null
+++ b/lib/graphiti/rails/rake_helpers.rb
@@ -0,0 +1,64 @@
+require "graphiti/rails/test_helpers"
+
+module Graphiti
+  module Rails
+    # Rake's `namespace` takes a block, and a block does not open a new definee,
+    # so helpers defined inside one are defined on Object and turn up in every
+    # request spec in the host app. They live here to stay off that namespace.
+    module RakeHelpers
+      extend TestHelpers
+
+      module_function
+
+      # ::Rails throughout, because bare Rails resolves to Graphiti::Rails here.
+      def session
+        @session ||= ActionDispatch::Integration::Session.new(::Rails.application)
+      end
+
+      # An engine runs the task from its own directory, not the host application's root.
+      def schema_path(path = nil)
+        path.nil? ? Graphiti.config.schema_path : File.expand_path(path, Dir.pwd)
+      end
+
+      def setup_rails!
+        ::Rails.application.eager_load!
+        ::Rails.application.config.cache_classes = true
+        ::Rails.application.config.action_controller.perform_caching = false
+      end
+
+      # The pool and RAILS_MAX_THREADS are read from this environment, and production may size both differently.
+      def connection_pool_advisory
+        return unless defined?(ActiveRecord::Base)
+
+        pool = ActiveRecord::Base.connection_db_config.pool
+        threads = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
+        sideload_threads = Graphiti.config.concurrency_max_threads
+        needed = threads + sideload_threads + 1
+        return if pool.nil? || pool >= needed
+
+        "WARNING database.yml pool is #{pool} in this environment. With concurrency on (the production default), " \
+          "each sideload holds its own connection, so pool should be at least " \
+          "web threads (#{threads}) + concurrency_max_threads (#{sideload_threads}) + 1 = #{needed}. " \
+          "If production sizes these differently, check the numbers there. " \
+          "See graphiti.dev/concepts/resources#concurrency-pool-sizing."
+      end
+
+      def make_request(path, debug = false)
+        if path.split("/").length == 2
+          path = "#{ApplicationResource.endpoint_namespace}#{path}"
+        end
+        path << if path.include?("?")
+          "&cache=bust"
+        else
+          "?cache=bust"
+        end
+        path = "#{path}&debug=true" if debug
+        handle_request_exceptions do
+          headers = {Authorization: ENV["AUTHORIZATION_HEADER"]}.compact
+          session.get(path.to_s, headers: headers)
+        end
+        JSON.parse(session.response.body)
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/rails/responders.rb b/lib/graphiti/rails/responders.rb
new file mode 100644
index 00000000..31dfaed5
--- /dev/null
+++ b/lib/graphiti/rails/responders.rb
@@ -0,0 +1,22 @@
+# For use with the `responders` gem responders gem to get respond_with
+module Graphiti
+  module Rails
+    module Responders
+      extend ActiveSupport::Concern
+
+      included do
+        include ActionController::MimeResponds
+
+        respond_to(*Graphiti::Rails.respond_to_formats)
+      end
+
+      # Override to avoid location url generation (for now)
+      def respond_with(*args, &blk)
+        opts = args.extract_options!
+        opts[:location] = nil
+        args << opts
+        super
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/rails/test_helpers.rb b/lib/graphiti/rails/test_helpers.rb
new file mode 100644
index 00000000..966bea55
--- /dev/null
+++ b/lib/graphiti/rails/test_helpers.rb
@@ -0,0 +1,22 @@
+module Graphiti
+  module Rails
+    # Wrap a request in `handle_request_exceptions` to assert on the rendered
+    # error payload. Exceptions propagate untouched in tests otherwise.
+    module TestHelpers
+      include RescueRegistry::RailsTestHelpers
+
+      # rescue_registry sets action_dispatch.show_exceptions to true/false.
+      # Rails 7.1 replaced those with :all/:rescuable/:none, and Rails 8 dropped
+      # boolean handling altogether. ExceptionWrapper#show? now treats
+      # everything that is not :none as "show", so passing false would leave
+      # exception handling on and silently do nothing.
+      def handle_request_exceptions(handle = true, &block)
+        super(handle ? :all : :none, &block)
+      end
+
+      def handle_request_exceptions?
+        ::Rails.application.config.action_dispatch.show_exceptions != :none
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/railtie.rb b/lib/graphiti/railtie.rb
index 5bcd5fa5..5eccec59 100644
--- a/lib/graphiti/railtie.rb
+++ b/lib/graphiti/railtie.rb
@@ -1,121 +1,8 @@
-module Graphiti
-  # @deprecated Use graphiti-rails's Graphiti::Rails::Railtie
-  class Railtie < ::Rails::Railtie
-    rake_tasks do
-      load File.expand_path("deprecated_tasks.rb", __dir__)
-    end
-
-    generators do
-      Dir[File.expand_path("../../deprecated_generators/**/*.rb", __dir__)].sort.each do |f|
-        require f
-      end
-    end
-
-    initializer "graphiti.require_activerecord_adapter" do
-      config.after_initialize do |app|
-        ActiveSupport.on_load(:active_record) do
-          require "graphiti/adapters/active_record"
-        end
-      end
-    end
-
-    initializer "graphti.logger" do
-      config.after_initialize do
-        Graphiti.logger = ::Rails.logger
-      end
-    end
-
-    initializer "graphiti.init" do
-      if ::Rails.application.config.eager_load
-        config.after_initialize do |app|
-          ::Rails.application.reload_routes!
-          Graphiti.setup!
-        end
-      end
-
-      if Mime[:jsonapi].nil? # rails 4
-        Mime::Type.register("application/vnd.api+json", :jsonapi)
-      end
-      register_parameter_parser
-      register_renderers
-      establish_concurrency
-      configure_endpoint_lookup
-    end
-
-    # from jsonapi-rails
-    PARSER = lambda do |body|
-      data = JSON.parse(body)
-      data[:format] = :jsonapi
-      data.with_indifferent_access
-    end
-
-    def register_parameter_parser
-      if ::Rails::VERSION::MAJOR >= 5
-        ActionDispatch::Request.parameter_parsers[:jsonapi] = PARSER
-      else
-        ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = PARSER
-      end
-    end
-
-    def register_renderers
-      ActiveSupport.on_load(:action_controller) do
-        ::ActionController::Renderers.add(:jsonapi) do |proxy, options|
-          self.content_type ||= Mime[:jsonapi]
-
-          # opts = {}
-          # if respond_to?(:default_jsonapi_render_options)
-          #   opts = default_jsonapi_render_options
-          # end
-
-          if proxy.is_a?(Hash) # for destroy
-            render(options.merge(json: proxy))
-          else
-            proxy.to_jsonapi(options)
-          end
-        end
-      end
-
-      ActiveSupport.on_load(:action_controller) do
-        ::ActionController::Renderers.add(:jsonapi_errors) do |proxy, options|
-          self.content_type ||= Mime[:jsonapi]
-
-          validation = GraphitiErrors::Validation::Serializer.new \
-            proxy.data, proxy.payload.relationships
-
-          render \
-            json: {errors: validation.errors},
-            status: :unprocessable_entity
-        end
-      end
-    end
-
-    # Only run concurrently if our environment supports it
-    def establish_concurrency
-      Graphiti.config.concurrency = !::Rails.env.test? &&
-        ::Rails.application.config.cache_classes
-    end
-
-    def configure_endpoint_lookup
-      Graphiti.config.context_for_endpoint = ->(path, action) {
-        method = :GET
-        case action
-          when :show then path = "#{path}/1"
-          when :create then method = :POST
-          when :update
-            path = "#{path}/1"
-            method = :PUT
-          when :destroy
-            path = "#{path}/1"
-            method = :DELETE
-        end
-
-        route = begin
-          ::Rails.application.routes.recognize_path(path, method: method)
-        rescue
-          nil
-        end
-        "#{route[:controller]}_controller".classify.safe_constantize if route
-      }
-    end
-  end
-end
+# Deprecated require path, kept so a leftover 1.x require does not raise.
+# The railtie lives in Graphiti::Rails as of 2.0. Remove in 3.0.
+require "graphiti"
+
+Graphiti::DEPRECATOR.warn(
+  'require "graphiti/railtie" is no longer needed. The railtie is part of the Rails ' \
+  "integration as of 2.0, and loads with graphiti when Rails is present."
+)
diff --git a/lib/graphiti/renderer.rb b/lib/graphiti/renderer.rb
index a8e3d6e6..b1f67dd1 100644
--- a/lib/graphiti/renderer.rb
+++ b/lib/graphiti/renderer.rb
@@ -13,8 +13,11 @@ def records
       @records ||= @proxy.data
     end
 
+    # JSON:API requires the key even when the include turns up nothing.
     def to_jsonapi
-      render(self.class.jsonapi_renderer).to_json
+      rendered = render(self.class.jsonapi_renderer)
+      rendered[:included] ||= [] if proxy.query.includes_requested?
+      rendered.to_json
     end
 
     def as_graphql
diff --git a/lib/graphiti/request_validators/update_validator.rb b/lib/graphiti/request_validators/update_validator.rb
index 684cd4e4..cb5a4ec9 100644
--- a/lib/graphiti/request_validators/update_validator.rb
+++ b/lib/graphiti/request_validators/update_validator.rb
@@ -2,7 +2,7 @@ module Graphiti
   module RequestValidators
     class UpdateValidator < Validator
       def validate
-        if required_payload? && payload_matches_endpoint?
+        if validate_data_shape && required_payload? && payload_matches_endpoint?
           super
         else
           false
@@ -13,11 +13,7 @@ def validate
 
       def attribute_mismatch(attr_path)
         @error_class = Graphiti::Errors::ConflictRequest
-        @errors.add(
-          attr_path.join("."),
-          :attribute_mismatch,
-          message: "does not match the server endpoint"
-        )
+        @errors.add(attr_path.join("."), :attribute_mismatch)
       end
 
       def required_payload?
@@ -32,7 +28,7 @@ def required_payload?
       end
 
       def payload_matches_endpoint?
-        unless @params.dig(:data, :id) == @params.dig(:filter, :id)
+        unless @params.dig(:data, :id).to_s == @params.dig(:filter, :id).to_s
           attribute_mismatch([:data, :id])
         end
 
diff --git a/lib/graphiti/request_validators/validator.rb b/lib/graphiti/request_validators/validator.rb
index 9076d96d..3026cd25 100644
--- a/lib/graphiti/request_validators/validator.rb
+++ b/lib/graphiti/request_validators/validator.rb
@@ -17,9 +17,9 @@ def validate
         # In the future these should have their own validation logic, but
         # for now we can just bypass
         return errors.blank? unless @params.has_key?(:data)
+        return false unless validate_data_shape
 
         resource = @root_resource
-
         if @params[:data].has_key?(:type)
           if (meta_type = deserialized_payload.meta[:type].try(:to_sym))
             if @root_resource.type != meta_type && @root_resource.polymorphic?
@@ -54,14 +54,22 @@ def validate_page
         page = @params[:page]
         return if page.nil? || page.respond_to?(:each_pair)
 
-        @errors.add(:page, :invalid, message: "must be an object")
+        @errors.add(:page, :invalid)
+      end
+
+      def validate_data_shape
+        data = @params[:data]
+        return true if data.nil? || data.respond_to?(:each_pair)
+
+        @errors.add(:data, :invalid)
+        false
       end
 
       def process_relationships(resource, relationships, payload_path)
         relationships.each_key do |name|
           unless resource.class.sideload(name.to_sym)
             full_key = fully_qualified_key(name, payload_path, :relationships)
-            @errors.add(full_key, :invalid_relationship, message: "is not a valid relationship")
+            @errors.add(full_key, :invalid_relationship)
           end
         end
 
@@ -102,11 +110,11 @@ def typecast_attributes(resource, attributes, action, payload_path)
             begin
               attributes[key] = resource.typecast(key, value, :writable)
             rescue Graphiti::Errors::UnknownAttribute
-              @errors.add(fully_qualified_key(key, payload_path), :unknown_attribute, message: "is an unknown attribute")
+              @errors.add(fully_qualified_key(key, payload_path), :unknown_attribute)
             rescue Graphiti::Errors::InvalidAttributeAccess
-              @errors.add(fully_qualified_key(key, payload_path), :unwritable_attribute, message: "cannot be written")
+              @errors.add(fully_qualified_key(key, payload_path), :unwritable_attribute)
             rescue Graphiti::Errors::TypecastFailed => e
-              @errors.add(fully_qualified_key(key, payload_path), :type_error, message: "should be type #{e.type_name}")
+              @errors.add(fully_qualified_key(key, payload_path), :type_error, type: e.type_name)
             end
           end
         end
diff --git a/lib/graphiti/resource.rb b/lib/graphiti/resource.rb
index 75a3289d..c882ad55 100644
--- a/lib/graphiti/resource.rb
+++ b/lib/graphiti/resource.rb
@@ -37,8 +37,8 @@ def decorate_record(record, index = nil)
       end
     end
 
-    def with_context(object, namespace = nil)
-      Graphiti.with_context(object, namespace) do
+    def with_context(object, action = nil)
+      Graphiti.with_context(object, action) do
         yield
       end
     end
@@ -51,8 +51,20 @@ def context
       self.class.context
     end
 
+    # Rails sets this from action_name, so it is whatever the controller action
+    # is called, not a fixed list. Persistence overrides it with :create/:update
+    # while saving, and :show while resolving sideloads afterwards.
+    def self.current_action
+      Graphiti.context[:action]
+    end
+
+    def current_action
+      self.class.current_action
+    end
+
     def self.context_namespace
-      Graphiti.context[:namespace]
+      Graphiti::DEPRECATOR.deprecation_warning(:context_namespace, "Use #current_action instead")
+      current_action
     end
 
     def context_namespace
@@ -123,9 +135,18 @@ def disassociate(parent, child, association_name, type)
       adapter.disassociate(parent, child, association_name, type)
     end
 
-    def persist_with_relationships(meta, attributes, relationships, caller_model = nil, foreign_key = nil)
-      persistence = Graphiti::Util::Persistence \
-        .new(self, meta, attributes, relationships, caller_model, foreign_key)
+    # TODO: make foreign_key a keyword once the satellite gems are rolled in - they call these positionally
+    def assign_with_relationships(meta, attributes, relationships, caller_model = nil, foreign_key = nil, model_instance: nil)
+      persistence = Graphiti::Util::Persistence
+        .new(self, meta, attributes, relationships, caller_model, foreign_key,
+          assigned_model: model_instance)
+      persistence.assign
+    end
+
+    def persist_with_relationships(meta, attributes, relationships, caller_model = nil, foreign_key = nil, assigned_model: nil)
+      persistence = Graphiti::Util::Persistence
+        .new(self, meta, attributes, relationships, caller_model, foreign_key,
+          assigned_model: assigned_model)
       persistence.run
     end
 
diff --git a/lib/graphiti/resource/configuration.rb b/lib/graphiti/resource/configuration.rb
index 7080d524..bab99308 100644
--- a/lib/graphiti/resource/configuration.rb
+++ b/lib/graphiti/resource/configuration.rb
@@ -4,8 +4,78 @@ module Configuration
       extend ActiveSupport::Concern
 
       DEFAULT_MAX_PAGE_SIZE = 1_000
+      LINK_MODES = [true, false, :on_demand].freeze
+      BELONGS_TO_RESOURCE_IDS_MODES = [:foreign_key, :always, :never].freeze
+      BLANK_MODES = [:literal, :null, :rejected].freeze
+
+      # Grouped for the ApplicationResource the install generator writes.
+      SETTING_GROUPS = { # :nodoc:
+        attributes: {
+          attributes_readable_by_default: {default: true},
+          attributes_writable_by_default: {default: true},
+          attributes_sortable_by_default: {default: true},
+          attributes_filterable_by_default: {default: true},
+          attributes_schema_by_default: {default: true},
+          typecast_reads: {default: true}
+        },
+        relationships: {
+          relationships_readable_by_default: {default: true},
+          relationships_writable_by_default: {default: true},
+          relationship_placeholders: {default: false, note: "render placeholders for relationships with no ids and no link"},
+          belongs_to_resource_ids_by_default: {
+            default: :foreign_key,
+            values: BELONGS_TO_RESOURCE_IDS_MODES,
+            invalid: ->(klass, value) { Errors::InvalidBelongsToResourceIds.new(klass, value) }
+          }
+        },
+        filters: {
+          filter_blanks_treated_as: {
+            default: :literal,
+            values: BLANK_MODES,
+            invalid: ->(klass, value) { Errors::InvalidFilterBlanks.new(klass, :filter_blanks_treated_as, value) }
+          }
+        },
+        sorting: {
+          default_sort: {default: nil, note: "per resource, e.g. [{id: :desc}]"}
+        },
+        pagination: {
+          page_default_size: {default: nil, note: "unset falls back to 20"},
+          page_max_size: {default: DEFAULT_MAX_PAGE_SIZE, format: "1_000"},
+          page_cursors: {default: false, note: "render cursors for pagination"},
+          page_links: {
+            default: false,
+            values: LINK_MODES,
+            invalid: ->(klass, value) { Errors::InvalidLinkRendering.new(klass, :page_links, value) }
+          }
+        },
+        endpoints: {
+          validate_requests: {default: true, note: "refuse requests to undeclared endpoints"}
+        },
+        links: {
+          relationship_links: {
+            default: true,
+            values: LINK_MODES,
+            invalid: ->(klass, value) { Errors::InvalidLinkRendering.new(klass, :relationship_links, value) }
+          },
+          validate_links: {default: true, note: "refuse to render links to unroutable endpoints"}
+        }
+      }.freeze
+
+      SETTINGS = SETTING_GROUPS.values.reduce(:merge).freeze # :nodoc:
 
       module Overrides
+        SETTINGS.each_pair do |name, setting|
+          next unless setting[:values]
+
+          define_method(:"#{name}=") do |val|
+            unless setting[:values].include?(val)
+              raise setting[:invalid].call(self, val)
+            end
+
+            super(val)
+          end
+        end
+
         def serializer=(val)
           if val
             if super(Class.new(val))
@@ -50,6 +120,7 @@ def adapter=(val)
         def remote=(val)
           super
           include ::Graphiti::Resource::Remote
+
           self.endpoint = {
             path: val,
             full_path: val,
@@ -84,49 +155,37 @@ class << self
           :polymorphic,
           :polymorphic_child,
           :serializer,
-          :default_page_size,
-          :default_sort,
-          :max_page_size,
-          :attributes_readable_by_default,
-          :attributes_writable_by_default,
-          :attributes_sortable_by_default,
-          :attributes_filterable_by_default,
-          :attributes_schema_by_default,
-          :relationships_readable_by_default,
-          :relationships_writable_by_default,
-          :filters_accept_nil_by_default,
-          :filters_deny_empty_by_default,
           :graphql_entrypoint,
-          :cursor_paginatable
+          *SETTINGS.keys
 
         class << self
           prepend Overrides
         end
 
+        SETTINGS.each_pair do |name, setting|
+          public_send(:"#{name}=", setting[:default])
+        end
+
         def self.inherited(klass)
           super
           klass.config = Util::Hash.deep_dup(config)
           klass.adapter ||= Adapters::Abstract
-          klass.max_page_size ||= DEFAULT_MAX_PAGE_SIZE
           # re-assigning causes a new Class.new
           klass.serializer = (klass.serializer || klass.infer_serializer_superclass)
           klass.type ||= klass.infer_type
           klass.graphql_entrypoint = klass.type.to_s.pluralize.to_sym
-          default(klass, :attributes_readable_by_default, true)
-          default(klass, :attributes_writable_by_default, true)
-          default(klass, :attributes_sortable_by_default, true)
-          default(klass, :attributes_filterable_by_default, true)
-          default(klass, :attributes_schema_by_default, true)
-          default(klass, :relationships_readable_by_default, true)
-          default(klass, :relationships_writable_by_default, true)
-          default(klass, :filters_accept_nil_by_default, false)
-          default(klass, :filters_deny_empty_by_default, false)
-
           unless klass.config[:attributes][:id]
             klass.attribute :id, :integer_id
           end
           klass.stat total: [:count]
 
+          # An abstract parent has no serializer for its sideloads, so the subclass applies them here.
+          if abstract_class?
+            klass.config[:sideloads].each_pair do |name, sideload|
+              klass.apply_sideload_to_serializer(name) if klass.eagerly_apply_sideload?(sideload)
+            end
+          end
+
           if defined?(::Rails) && ::Rails.env.development?
             # Avoid adding dupe resources when re-autoloading
             Graphiti.resources.reject! { |r| r.name == klass.name }
@@ -136,6 +195,52 @@ def self.inherited(klass)
       end
 
       class_methods do
+        # Deprecated. Both folded into filter_blanks_treated_as. Remove in 3.0.
+        def filters_accept_nil_by_default
+          filter_blanks_treated_as == :null
+        end
+
+        def filters_accept_nil_by_default=(val)
+          self.filter_blanks_treated_as = val ? :null : :literal
+        end
+
+        def filters_deny_empty_by_default
+          filter_blanks_treated_as == :rejected
+        end
+
+        def filters_deny_empty_by_default=(val)
+          self.filter_blanks_treated_as = val ? :rejected : :literal
+        end
+
+        # Deprecated. Renamed to the page_ family. Remove in 3.0.
+        def default_page_size
+          page_default_size
+        end
+
+        def default_page_size=(val)
+          self.page_default_size = val
+        end
+
+        def max_page_size
+          page_max_size
+        end
+
+        def max_page_size=(val)
+          self.page_max_size = val
+        end
+
+        def cursor_paginatable
+          page_cursors
+        end
+
+        def cursor_paginatable=(val)
+          self.page_cursors = val
+        end
+
+        def cursor_paginatable?
+          !!page_cursors
+        end
+
         def get_attr!(name, flag, opts = {})
           opts[:raise_error] = true
           get_attr(name, flag, opts)
@@ -192,14 +297,6 @@ def infer_serializer_superclass
           serializer_class
         end
 
-        def default(object, attr, value)
-          prior = object.send(attr)
-          unless prior || prior == false
-            object.send(:"#{attr}=", value)
-          end
-        end
-        private :default
-
         def config
           @config ||=
             {
@@ -319,5 +416,20 @@ def default_filters
         self.class.default_filters
       end
     end
+
+    blanks_msg = "Use `self.filter_blanks_treated_as` (:literal, :null, or :rejected)"
+    page_msg = "Use `self.page_default_size`, `self.page_max_size` and `self.page_cursors`"
+    DEPRECATOR.deprecate_methods(Configuration::ClassMethods,
+      filters_accept_nil_by_default: blanks_msg,
+      "filters_accept_nil_by_default=": blanks_msg,
+      filters_deny_empty_by_default: blanks_msg,
+      "filters_deny_empty_by_default=": blanks_msg,
+      default_page_size: page_msg,
+      "default_page_size=": page_msg,
+      max_page_size: page_msg,
+      "max_page_size=": page_msg,
+      cursor_paginatable: page_msg,
+      "cursor_paginatable=": page_msg,
+      cursor_paginatable?: page_msg)
   end
 end
diff --git a/lib/graphiti/resource/dsl.rb b/lib/graphiti/resource/dsl.rb
index 8d073b8f..48c0bb56 100644
--- a/lib/graphiti/resource/dsl.rb
+++ b/lib/graphiti/resource/dsl.rb
@@ -9,7 +9,7 @@ def filter(name, *args, &blk)
           opts = args.extract_options!
           type_override = args[0]
 
-          if (att = (attributes[name] || extra_attributes[name]))
+          if (att = attributes[name] || extra_attributes[name])
             # We're opting in to filtering, so force this
             # UNLESS the filter is guarded at the attribute level
             att[:filterable] = true if att[:filterable] == false
@@ -27,7 +27,7 @@ def filter(name, *args, &blk)
             end
 
             required = att[:filterable] == :required || !!opts[:required]
-            schema = !!opts[:via_attribute_dsl] ? att[:schema] : opts[:schema] != false
+            schema = (!!opts[:via_attribute_dsl]) ? att[:schema] : opts[:schema] != false
 
             config[:filters][name.to_sym] = {
               aliases: aliases,
@@ -40,8 +40,7 @@ def filter(name, *args, &blk)
               required: required,
               schema: schema,
               operators: operators.to_hash,
-              allow_nil: opts.fetch(:allow_nil, filters_accept_nil_by_default),
-              deny_empty: opts.fetch(:deny_empty, filters_deny_empty_by_default)
+              blanks: blanks_for(name, opts)
             }
           elsif (type = args[0])
             attribute name, type, only: [:filterable], allow: opts[:allow]
@@ -206,6 +205,26 @@ def relationship_option(options, name)
           end
         end
         private :relationship_option
+
+        def blanks_for(name, opts)
+          if opts.key?(:deny_empty)
+            DEPRECATOR.warn("The deny_empty: filter option is deprecated. Use blanks: :rejected.")
+            return :rejected if opts[:deny_empty]
+          end
+
+          if opts.key?(:allow_nil)
+            DEPRECATOR.warn("The allow_nil: filter option is deprecated. Use blanks: :null.")
+            return opts[:allow_nil] ? :null : :literal
+          end
+
+          blanks = opts.fetch(:blanks, filter_blanks_treated_as)
+          unless Resource::BLANK_MODES.include?(blanks)
+            raise Errors::InvalidFilterBlanks.new(self, name, blanks)
+          end
+
+          blanks
+        end
+        private :blanks_for
       end
     end
   end
diff --git a/lib/graphiti/resource/interface.rb b/lib/graphiti/resource/interface.rb
index 58ba96a2..efb580ad 100644
--- a/lib/graphiti/resource/interface.rb
+++ b/lib/graphiti/resource/interface.rb
@@ -17,7 +17,7 @@ def all(params = {}, base_scope = nil)
 
         # @api private
         def _all(params, opts, base_scope)
-          runner = Runner.new(self, params, opts.delete(:query), :all)
+          runner = Runner.new(self, params, query: opts.delete(:query), action: :all)
           opts[:params] = params
           runner.proxy(base_scope, opts.merge(caching_options))
         end
@@ -36,7 +36,7 @@ def _find(params = {}, base_scope = nil)
           params[:filter] ||= {}
           params[:filter][:id] = id if id
 
-          runner = Runner.new(self, params, nil, :find)
+          runner = Runner.new(self, params, action: :find)
 
           find_options = {
             single: true,
@@ -50,13 +50,13 @@ def _find(params = {}, base_scope = nil)
         def build(params, base_scope = nil)
           validate_request!(params)
           runner = Runner.new(self, params)
-          runner.proxy(base_scope, single: true, raise_on_missing: true)
+          runner.proxy(base_scope, single: true, raise_on_missing: true, assign_action: :create)
         end
 
         # Wrap models fetched outside graphiti so they render like any other proxy
         def wrap(models, base_scope = nil)
           validate_wrap_models!(models)
-          runner = Runner.new(self, {}, nil, :find)
+          runner = Runner.new(self, {}, action: :find)
           runner.proxy(base_scope, bypass_required_filters: true).tap do |proxy|
             proxy.data = models
           end
@@ -80,12 +80,12 @@ def caching_options
         end
 
         def validate_request!(params)
-          return if Graphiti.context[:graphql] || !validate_endpoints?
+          return if Graphiti.context[:graphql] || !validate_requests?
 
           if context&.respond_to?(:request)
             path = context.request.env["PATH_INFO"]
-            unless allow_request?(path, params, context_namespace)
-              raise Errors::InvalidEndpoint.new(self, path, context_namespace)
+            unless allow_request?(path, params, current_action)
+              raise Errors::InvalidEndpoint.new(self, path, current_action)
             end
           end
         end
diff --git a/lib/graphiti/resource/links.rb b/lib/graphiti/resource/links.rb
index f8221fa6..7754172b 100644
--- a/lib/graphiti/resource/links.rb
+++ b/lib/graphiti/resource/links.rb
@@ -25,12 +25,8 @@ def endpoint=(value)
       class_attribute :endpoint,
         :base_url,
         :endpoint_namespace,
-        :secondary_endpoints,
-        :autolink,
-        :validate_endpoints
+        :secondary_endpoints
       self.secondary_endpoints = []
-      self.autolink = true
-      self.validate_endpoints = true
 
       class << self
         prepend Overrides
@@ -38,10 +34,40 @@ class << self
     end
 
     class_methods do
+      # Deprecated. Folded into relationship_links. Remove in 3.0.
+      def autolink
+        relationship_links != false
+      end
+
+      # true cannot express :on_demand, so it leaves an inherited on-demand mode alone.
+      def autolink=(val)
+        return if val && relationship_links == :on_demand
+
+        self.relationship_links = val ? true : false
+      end
+
+      def autolink?
+        autolink
+      end
+
+      # Deprecated. Split into validate_requests and validate_links. Remove in 3.0.
+      def validate_endpoints
+        validate_requests && validate_links
+      end
+
+      def validate_endpoints=(val)
+        self.validate_requests = val
+        self.validate_links = val
+      end
+
+      def validate_endpoints?
+        validate_endpoints
+      end
+
       def infer_endpoint
         return unless name
 
-        path = "/#{name.gsub("Resource", "").pluralize.underscore}".to_sym
+        path = :"/#{name.gsub("Resource", "").pluralize.underscore}"
         {
           path: path,
           full_path: full_path_for(path),
@@ -101,4 +127,12 @@ def url_for(path)
       end
     end
   end
+
+  msg = "Use `self.relationship_links` (true, false, or :on_demand)"
+  endpoints_msg = "Use `self.validate_requests` for inbound requests, `self.validate_links` for rendered links"
+  DEPRECATOR.deprecate_methods(Links::ClassMethods,
+    autolink: msg, "autolink=": msg, autolink?: msg,
+    validate_endpoints: endpoints_msg,
+    "validate_endpoints=": endpoints_msg,
+    validate_endpoints?: endpoints_msg)
 end
diff --git a/lib/graphiti/resource/persistence.rb b/lib/graphiti/resource/persistence.rb
index 39d3736d..48d0d22f 100644
--- a/lib/graphiti/resource/persistence.rb
+++ b/lib/graphiti/resource/persistence.rb
@@ -65,50 +65,73 @@ def around_destroy(method = nil, &blk)
         def add_callback(kind, lifecycle, method, only, &blk)
           config[:callbacks][kind] ||= {}
           config[:callbacks][kind][lifecycle] ||= []
-          config[:callbacks][kind][lifecycle] << {callback: (method || blk), only: Array(only)}
+          config[:callbacks][kind][lifecycle] << {callback: method || blk, only: Array(only)}
         end
       end
 
-      def create(create_params, meta = nil)
-        model_instance = nil
-        snapshot = attributes_snapshot(:create, create_params)
-
-        run_callbacks :persistence, :create, create_params, meta do
-          warn_attributes_mutated_in_around_persistence(:create, snapshot, create_params)
+      # +model_instance+ is the already-built model from a previous #assign
+      # (see ResourceProxy#assign_attributes). When given, attributes are
+      # applied to it rather than to a freshly built/found model.
+      def assign(assign_params, meta = nil, action_name = nil, model_instance: nil)
+        # Only update strips :id (it identifies the record to find) - a create
+        # payload may legitimately carry a client-supplied id to assign.
+        if action_name == :update
+          id = assign_params[:id]
+          assign_params = assign_params.except(:id)
+        end
 
-          run_callbacks :attributes, :create, create_params, meta do |params|
-            model_instance = call_with_meta(:build, model, meta)
-            call_with_meta(:assign_attributes, model_instance, params, meta)
-            model_instance
+        run_callbacks :attributes, action_name, assign_params, meta do |params|
+          model_instance ||= if action_name == :update
+            self.class._find(id: id).data
+          else
+            call_with_meta(:build, model, meta)
           end
+          call_with_meta(:assign_attributes, model_instance, params, meta)
+          model_instance
+        end
+
+        model_instance
+      end
+
+      # The model built by a prior ResourceProxy#assign_attributes, present
+      # for the duration of the save that persists it
+      attr_reader :assigned_model
+
+      # @api private
+      def with_assigned_model(model)
+        @assigned_model = model
+        yield
+      ensure
+        @assigned_model = nil
+      end
 
+      # Attributes are assigned before the persistence callbacks fire, so
+      # around_persistence receives the assigned model - its pre-yield
+      # position is the last chance to touch the model before save, inside
+      # the transaction. Modify attributes in before_attributes instead.
+      def create(create_params, meta = nil)
+        model_instance = assigned_model || assign(create_params, meta, :create)
+
+        run_callbacks :persistence, :create, model_instance, meta do
           run_callbacks :save, :create, model_instance, meta do
             model_instance = call_with_meta(:save, model_instance, meta)
           end
 
           model_instance
         end
+
+        model_instance
       end
 
       def update(update_params, meta = nil)
-        model_instance = nil
-        id = update_params[:id]
-        update_params = update_params.except(:id)
-
-        snapshot = attributes_snapshot(:update, update_params)
-
-        run_callbacks :persistence, :update, update_params, meta do
-          warn_attributes_mutated_in_around_persistence(:update, snapshot, update_params)
-
-          run_callbacks :attributes, :update, update_params, meta do |params|
-            model_instance = self.class._find(id: id).data
-            call_with_meta(:assign_attributes, model_instance, params, meta)
-            model_instance
-          end
+        model_instance = assigned_model || assign(update_params, meta, :update)
 
+        run_callbacks :persistence, :update, model_instance, meta do
           run_callbacks :save, :update, model_instance, meta do
             model_instance = call_with_meta(:save, model_instance, meta)
           end
+
+          model_instance
         end
 
         model_instance
@@ -140,37 +163,6 @@ def delete(model_instance, meta = nil)
 
       private
 
-      # In Graphiti 2.0, attributes are assigned to the model before the
-      # persistence hooks fire, so hash modifications made by an
-      # around_persistence hook before its yield will no longer be applied.
-      # Snapshot-and-compare detects exactly those hooks: the comparison
-      # happens before any attributes-phase callback has run, so hooks that
-      # only wrap their yield never trigger the warning.
-      def attributes_snapshot(action, params)
-        return unless around_persistence_hooks?(action)
-        Util::Hash.deep_dup(params)
-      end
-
-      def around_persistence_hooks?(action)
-        hooks = self.class.config[:callbacks][:persistence].try(:[], :around) || []
-        hooks.any? { |hook| hook[:only].include?(action) }
-      end
-
-      def warn_attributes_mutated_in_around_persistence(action, snapshot, params)
-        return if snapshot.nil? || snapshot == params
-
-        changed_keys = (snapshot.keys | params.keys).reject { |key| snapshot[key] == params[key] }
-        Graphiti::DEPRECATOR.warn(<<~MSG)
-          #{self.class}'s around_persistence hook modified the attributes hash before yield (changed keys: #{changed_keys.map(&:inspect).join(", ")}). In Graphiti 2.0, attributes are assigned to the model before persistence hooks run, and these modifications will be silently ignored. Move the modification to a hook that runs before assignment:
-
-            before_attributes do |attributes|
-              attributes[#{changed_keys.first.inspect}] = ...
-            end
-
-          or set the value on the model itself in a before_save hook. See UPGRADING.md in the Graphiti 2.0 release.
-        MSG
-      end
-
       def run_callbacks(kind, action, *args)
         fire_around_callbacks(kind, action, *args) do |*yieldargs|
           fire_callbacks(kind, :before, action, *yieldargs)
diff --git a/lib/graphiti/resource/polymorphism.rb b/lib/graphiti/resource/polymorphism.rb
index e5d73e63..b038f8d6 100644
--- a/lib/graphiti/resource/polymorphism.rb
+++ b/lib/graphiti/resource/polymorphism.rb
@@ -56,7 +56,7 @@ def sideload(name)
             on_type, name = split_on[1].split("--").map(&:to_sym)
           end
 
-          sl = super(name)
+          sl = super
           if !polymorphic_child? && sl.nil?
             children.each do |c|
               next if on_type && c.type != on_type
diff --git a/lib/graphiti/resource/remote.rb b/lib/graphiti/resource/remote.rb
index d4d81e29..c8c500d1 100644
--- a/lib/graphiti/resource/remote.rb
+++ b/lib/graphiti/resource/remote.rb
@@ -6,7 +6,8 @@ module Remote
       included do
         self.adapter = Graphiti::Adapters::GraphitiAPI
         self.model = OpenStruct
-        self.validate_endpoints = false
+        self.validate_requests = false
+        self.validate_links = false
 
         class_attribute :timeout,
           :open_timeout
@@ -38,8 +39,8 @@ def before_resolve(scope, query)
       # Forward all headers
       def request_headers
         {}.tap do |headers|
-          # TODO: Maybe handle this in graphiti-rails
-          if defined?(Rails) && context
+          # TODO: Maybe handle this in the Rails integration
+          if defined?(::Rails) && context
             raw = context.request.headers.to_h
             if (auth = raw["HTTP_AUTHORIZATION"])
               headers["Authorization"] = auth
diff --git a/lib/graphiti/resource/sideloading.rb b/lib/graphiti/resource/sideloading.rb
index 5398562b..47e11357 100644
--- a/lib/graphiti/resource/sideloading.rb
+++ b/lib/graphiti/resource/sideloading.rb
@@ -126,11 +126,11 @@ def association_types(memo = [])
         end
 
         # If eager loading, ensure routes are loaded first, then apply
-        # This happens in Railtie
+        # This happens in Railtie. Setup runs once, so classes redefined by a reload apply at definition.
         def eagerly_apply_sideload?(sideload)
-          # TODO: Maybe handle this in graphiti-rails
+          # TODO: Maybe handle this in the Rails integration
           if defined?(::Rails) && (app = ::Rails.application)
-            app.config.eager_load ? false : true
+            app.config.eager_load ? Graphiti.setup? : true
           else
             sideload.resource_class_loaded?
           end
diff --git a/lib/graphiti/resource_proxy.rb b/lib/graphiti/resource_proxy.rb
index f94be876..0a5effdb 100644
--- a/lib/graphiti/resource_proxy.rb
+++ b/lib/graphiti/resource_proxy.rb
@@ -11,17 +11,18 @@ def initialize(
       payload: nil,
       single: false,
       raise_on_missing: false,
+      assign_action: nil,
       cache: nil,
       cache_expires_in: nil,
       cache_tag: nil
     )
-
       @resource = resource
       @scope = scope
       @query = query
       @payload = payload
       @single = single
       @raise_on_missing = raise_on_missing
+      @assign_action = assign_action
       @cache = cache
       @cache_expires_in = cache_expires_in
       @cache_tag = cache_tag
@@ -87,7 +88,10 @@ def data=(models)
     end
 
     def data
-      @data ||= begin
+      return @data unless @data.nil?
+      return assign_attributes(@payload.params) if @assign_action
+
+      @data = begin
         records = @scope.resolve
         raise Graphiti::Errors::RecordNotFound if records.empty? && raise_on_missing?
 
@@ -95,6 +99,7 @@ def data
         records
       end
     end
+
     alias_method :to_a, :data
     alias_method :resolve_data, :data
 
@@ -137,26 +142,68 @@ def pagination
       @pagination ||= Delegates::Pagination.new(self)
     end
 
+    # Apply request params to the underlying model without saving it,
+    # Rails-style: the params are always passed explicitly, in the same
+    # request-params shape find/build accept. They are validated,
+    # deserialized, and become the payload #save will persist.
+    #
+    # Idempotent per params - calling again with params that normalize to
+    # the same payload is a no-op, so the attributes callbacks fire once.
+    # Different params re-assign onto the same model instance.
+    #
+    # Note the attributes callbacks fire here, outside any transaction
+    # opened during #save - the persistence hooks wrap only the save phase,
+    # receiving this assigned model.
+    def assign_attributes(params)
+      action = @assign_action || :update
+      params = normalized_params_copy(params)
+      add_endpoint_filter(params, action)
+      validator = ::Graphiti::RequestValidator.new(@resource, params, action)
+      with_context_action(action) { validator.validate! }
+
+      if @assigned_model && same_write_payload?(validator.deserialized_payload)
+        return @assigned_model
+      end
+
+      @payload = validator.deserialized_payload
+      @assigned_model = @data = @resource.assign_with_relationships(
+        @payload.meta(action: action),
+        @payload.attributes,
+        @payload.relationships,
+        model_instance: @assigned_model || (data if action == :update)
+      )
+    end
+
+    # TODO: remove this. Only used for persisting many-to-many with AR
+    # (see activerecord adapter)
+    def with_context_action(action)
+      original = Graphiti.context[:action]
+      Graphiti.context[:action] = action
+      yield
+    ensure
+      Graphiti.context[:action] = original
+    end
+
     def save(action: :create)
-      # TODO: remove this. Only used for persisting many-to-many with AR
-      # (see activerecord adapter)
-      original = Graphiti.context[:namespace]
-      begin
-        Graphiti.context[:namespace] = action
-        ::Graphiti::RequestValidator.new(@resource, @payload.params, action).validate!
-        validator = persist {
+      validator = with_context_action(action) do
+        # An assigned model can only come from #assign_attributes, which
+        # validated the payload it stored - re-validating here would run the
+        # writable guards (and their guard_model lookups) a redundant time.
+        unless @assigned_model
+          ::Graphiti::RequestValidator.new(@resource, @payload.params, action).validate!
+        end
+        persist {
           @resource.persist_with_relationships \
             @payload.meta(action: action),
             @payload.attributes,
-            @payload.relationships
+            @payload.relationships,
+            assigned_model: @assigned_model
         }
-      ensure
-        Graphiti.context[:namespace] = original
       end
       @data, success = validator.to_a
 
       if success
-        # If the context namespace is `update` or `create`, certain
+        # If the context action is `update` or `create`, certain
         # adapters will cause N+1 validation calls, so lets explicitly
         # switch to a lookup context.
         Graphiti.with_context(Graphiti.context[:object], :show) do
@@ -185,12 +232,16 @@ def destroy
       success
     end
 
-    def update
+    # Rails-style: pass params to assign and save in one call, or call with
+    # no arguments to save a payload assigned earlier (via find or
+    # #assign_attributes).
+    def update(params = nil)
+      assign_attributes(params) if params
       resolve_data
       save(action: :update)
     end
 
-    alias update_attributes update # standard:disable Style/Alias
+    alias_method :update_attributes, :update
 
     def include_hash
       @include_hash ||= begin
@@ -247,6 +298,39 @@ def cache_key_with_version
 
     private
 
+    # Validation typecasts values and injects ids into the params it is
+    # given - work on a deep copy so the caller's hash stays untouched.
+    def normalized_params_copy(params)
+      if params.respond_to?(:to_unsafe_h)
+        params.to_unsafe_h.deep_symbolize_keys
+      else
+        ::Graphiti::Util::Hash.deep_dup(params)
+      end
+    end
+
+    # UpdateValidator enforces that data.id matches the endpoint's filter id.
+    # Params that came through find already carry that filter; params passed
+    # directly to #assign_attributes usually don't, so merge in the id this
+    # proxy was found with. A payload whose data.id names a different record
+    # still fails validation with ConflictRequest. Mutates the copy made by
+    # #normalized_params_copy, never caller state.
+    def add_endpoint_filter(params, action)
+      return unless action == :update
+
+      endpoint_id = @query.filters[:id]
+      return if endpoint_id.nil? || params[:filter].try(:[], :id)
+
+      params[:filter] ||= {}
+      params[:filter][:id] = endpoint_id
+    end
+
+    # Compare only the write payload (data + included) - a repeat call whose
+    # params differ in read-side keys like sort or page is still a no-op.
+    def same_write_payload?(deserialized_payload)
+      deserialized_payload.params.values_at(:data, :included) ==
+        @payload.params.values_at(:data, :included)
+    end
+
     def persist
       transaction_response = @resource.transaction do
         ::Graphiti::Util::TransactionHooksRecorder.record do
diff --git a/lib/graphiti/responders.rb b/lib/graphiti/responders.rb
index 2025e5e0..e7e6789f 100644
--- a/lib/graphiti/responders.rb
+++ b/lib/graphiti/responders.rb
@@ -1,21 +1,8 @@
-# If you're using Rails + responders gem to get respond_with
-module Graphiti
-  module Responders
-    extend ActiveSupport::Concern
+# Deprecated entry point. graphiti required this path itself in 1.x, so an
+# explicit require left behind would otherwise raise LoadError. Remove in 3.0.
+require "graphiti/rails"
 
-    included do
-      backtrace = ::Rails::VERSION::MAJOR == 4 ? caller(2) : caller_locations(2)
-      DEPRECATOR.deprecation_warning("Including Graphiti::Responders", "Use graphiti-rails instead. See https://www.graphiti.dev/guides/graphiti-rails-migration for details.", backtrace)
-      include ActionController::MimeResponds
-      respond_to(*Graphiti.config.respond_to)
-    end
-
-    # Override to avoid location url generation (for now)
-    def respond_with(*args, &blk)
-      opts = args.extract_options!
-      opts[:location] = nil
-      args << opts
-      super(*args, &blk)
-    end
-  end
-end
+Graphiti::DEPRECATOR.warn(
+  'require "graphiti/responders" is deprecated. Use "graphiti/rails" and include ' \
+  "Graphiti::Rails::Responders."
+)
diff --git a/lib/graphiti/runner.rb b/lib/graphiti/runner.rb
index bce0c26b..8bad38ea 100644
--- a/lib/graphiti/runner.rb
+++ b/lib/graphiti/runner.rb
@@ -3,12 +3,17 @@ class Runner
     attr_reader :params
     attr_reader :deserialized_payload
 
-    def initialize(resource_class, params, query = nil, action = nil)
+    def initialize(resource_class, params, *positional, query: nil, action: nil)
+      if positional.any?
+        Graphiti::DEPRECATOR.warn("Passing query/action to Runner.new positionally is deprecated. Use query:/action: keywords.")
+        query ||= positional[0]
+        action ||= positional[1]
+      end
+
       @resource_class = resource_class
       @params = params
       @query = query
       @action = action
-
       validator = RequestValidator.new(jsonapi_resource, params, action)
       validator.validate!
 
@@ -30,7 +35,7 @@ def jsonapi_context
     end
 
     def query
-      @query ||= Query.new(jsonapi_resource, params, nil, nil, [], @action)
+      @query ||= Query.new(jsonapi_resource, params, action: @action)
     end
 
     def query_hash
@@ -77,6 +82,7 @@ def proxy(base = nil, opts = {})
         payload: deserialized_payload,
         single: opts[:single],
         raise_on_missing: opts[:raise_on_missing],
+        assign_action: opts[:assign_action],
         cache: opts[:cache],
         cache_expires_in: opts[:cache_expires_in],
         cache_tag: opts[:cache_tag]
diff --git a/lib/graphiti/schema.rb b/lib/graphiti/schema.rb
index 70330f6c..305e3175 100644
--- a/lib/graphiti/schema.rb
+++ b/lib/graphiti/schema.rb
@@ -3,7 +3,7 @@ class Schema
     attr_reader :resources
 
     def self.generate(resources = nil)
-      # TODO: Maybe handle this in graphiti-rails
+      # TODO: Maybe handle this in the Rails integration
       ::Rails.application.eager_load! if defined?(::Rails)
       resources ||= Graphiti.resources.reject(&:abstract_class?)
       resources.reject! { |r| r.name.nil? }
@@ -11,19 +11,18 @@ def self.generate(resources = nil)
       new(resources).generate
     end
 
-    def self.generate!(resources = nil)
-      schema = generate(resources)
+    def self.generate!(resources = nil, path: Graphiti.config.schema_path, force: ENV["FORCE_SCHEMA"] == "true")
+      result = check(resources, path: path)
+      return result.errors if !force && !result.compatible?
 
-      if ENV["FORCE_SCHEMA"] != "true" && File.exist?(Graphiti.config.schema_path)
-        old = JSON.parse(File.read(Graphiti.config.schema_path))
-        errors = Graphiti::SchemaDiff.new(old, schema).compare
-        return errors if errors.any?
-      end
-      FileUtils.mkdir_p(Graphiti.config.schema_path.to_s.gsub("/schema.json", ""))
-      File.write(Graphiti.config.schema_path, JSON.pretty_generate(schema))
+      result.write!
       []
     end
 
+    def self.check(resources = nil, path: Graphiti.config.schema_path)
+      Check.new(generate(resources), path)
+    end
+
     def initialize(resources)
       @resources = resources.sort_by(&:name)
       @remote_resources = @resources.select(&:remote?)
@@ -111,8 +110,8 @@ def generate_resources
           config[:default_sort] = default_sort
         end
 
-        if r.default_page_size
-          config[:default_page_size] = r.default_page_size
+        if r.page_default_size
+          config[:default_page_size] = r.page_default_size
         end
 
         if r.polymorphic? && !r.polymorphic_child?
@@ -251,6 +250,10 @@ def relationships(resource)
             schema[:guard] = true
           end
 
+          if config.render_resource_ids?
+            schema[:linkage] = true
+          end
+
           r[name] = schema
         end
       end
diff --git a/lib/graphiti/schema/check.rb b/lib/graphiti/schema/check.rb
new file mode 100644
index 00000000..933da45d
--- /dev/null
+++ b/lib/graphiti/schema/check.rb
@@ -0,0 +1,69 @@
+module Graphiti
+  class Schema
+    class Check
+      attr_reader :path, :errors
+
+      def initialize(schema, path)
+        @path = path
+        @schema = schema
+        @generated = normalize(schema)
+        @committed = normalize(JSON.parse(File.read(path))) if File.exist?(path)
+        @errors = @committed ? SchemaDiff.new(@committed, @generated).compare : []
+      end
+
+      def missing?
+        @committed.nil?
+      end
+
+      def stale?
+        !missing? && @committed != @generated
+      end
+
+      def compatible?
+        errors.empty?
+      end
+
+      def ok?
+        !missing? && !stale? && compatible?
+      end
+
+      def write!
+        FileUtils.mkdir_p(File.dirname(path))
+        File.write(path, JSON.pretty_generate(@schema))
+        path
+      end
+
+      def message
+        return "Schema is up to date: #{path}" if ok?
+        return "#{missing_message}\n\n#{regenerate}" if missing?
+        return "#{incompatible_message}\n\n#{errors.join("\n")}" unless compatible?
+
+        "Schema file is outdated: #{path}\n\n#{regenerate}"
+      end
+
+      private
+
+      def normalize(schema)
+        JSON.parse(JSON.generate(schema))
+      end
+
+      def missing_message
+        "Schema file not found: #{path}"
+      end
+
+      def incompatible_message
+        <<~MSG.chomp
+          Found backwards-incompatibilities in schema: #{path}
+
+          Re-run with FORCE_SCHEMA=true to accept them and overwrite the file.
+
+          Incompatibilities:
+        MSG
+      end
+
+      def regenerate
+        "Run `rake graphiti:schema:generate` and commit the file."
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/schema_diff.rb b/lib/graphiti/schema_diff.rb
index 525b619a..fa12df0a 100644
--- a/lib/graphiti/schema_diff.rb
+++ b/lib/graphiti/schema_diff.rb
@@ -106,6 +106,10 @@ def compare_relationships(old_resource, new_resource)
           @errors << "#{old_resource[:name]}: relationship #{name.inspect} became guarded."
         end
 
+        if old_rel[:linkage] && !new_rel[:linkage]
+          @errors << "#{old_resource[:name]}: relationship #{name.inspect} no longer includes resource linkage."
+        end
+
         if new_rel[:resource] != old_rel[:resource]
           @errors << "#{old_resource[:name]}: relationship #{name.inspect} changed resource from #{old_rel[:resource]} to #{new_rel[:resource]}."
         end
diff --git a/lib/graphiti/scope.rb b/lib/graphiti/scope.rb
index 65a98f81..b104252a 100644
--- a/lib/graphiti/scope.rb
+++ b/lib/graphiti/scope.rb
@@ -39,18 +39,17 @@ def self.resolve_synchronously?
       !Graphiti.config.concurrency || on_pool_thread?
     end
 
-    # TODO: move to Fiber[] once the floor is Ruby 3.2
     def self.on_pool_thread?
-      Thread.current[POOL_THREAD] == true
+      Fiber[POOL_THREAD] == true
     end
 
     # Restores rather than clears because :caller_runs may have run the task on a request thread.
     def self.marking_pool_thread
-      previous = Thread.current[POOL_THREAD]
-      Thread.current[POOL_THREAD] = true
+      previous = Fiber[POOL_THREAD]
+      Fiber[POOL_THREAD] = true
       yield
     ensure
-      Thread.current[POOL_THREAD] = previous
+      Fiber[POOL_THREAD] = previous
     end
 
     def initialize(object, resource, query, opts = {})
@@ -59,18 +58,18 @@ def initialize(object, resource, query, opts = {})
       @query = query
       @opts = opts
 
+      @resolved_sideload_proxies = {}
+      @resolved_sideload_proxies_lock = Mutex.new
+
       @object = @resource.around_scoping(@object, @query.hash) { |scope|
         apply_scoping(scope, opts)
       }
     end
 
     def resolve(&blk)
-      # When concurrency is disabled, take a synchronous path that mirrors the
-      # pre-1.8 semantics. This avoids allocating Concurrent::Promises futures,
-      # Thread/Fiber storage snapshots, and Rails executor wrappers on every
-      # request purely to drive a thread pool that is intentionally synchronous.
+      # The caller blocks on .value! either way, so concurrency only benefits parallel sideloads
       # See https://github.com/graphiti-api/graphiti/issues/505
-      if self.class.resolve_synchronously?
+      if self.class.resolve_synchronously? || !overlapping_sideloads?
         sync_resolve(&blk)
       else
         future_resolve(&blk).value!
@@ -83,6 +82,9 @@ def resolve_sideloads(results)
       else
         future_resolve_sideloads(results).value!
       end
+
+      # Never return the sideloads hash, a caller mutating it would mess up the cache key
+      nil
     end
 
     def future_resolve(&blk)
@@ -151,17 +153,18 @@ def sync_resolve(&blk)
     def sync_resolve_sideloads(results)
       return if results == []
 
-      each_applicable_sideload do |sideload, sideload_query|
+      reset_captured_sideload_proxies
+      each_applicable_sideload do |name, sideload, sideload_query|
         Graphiti.config.before_sideload&.call(Graphiti.context)
-        sideload.resolve(results, sideload_query, @resource)
+        sideload.sync_resolve(results, sideload_query, @resource) do |proxy|
+          capture_sideload_proxy(name, proxy)
+        end
       end
-
-      # resolve_sideloads is public, and without this it would return @query.sideloads itself,
-      # where a delete would silently drop that sideload from the cache key.
-      nil
     end
 
-    # Runs inline on the calling thread in both the sync and future paths.
+    # Resolve this scope's own data: run hooks, resolve the resource, and
+    # decorate the results. Shared by the sync and future paths — everything
+    # here runs inline on the calling thread in both modes.
     def resolve_primary_data
       resolved = broadcast_data { |payload|
         @object = @resource.before_resolve(@object, @query)
@@ -169,10 +172,53 @@ def resolve_primary_data
         payload[:results]
       }
       resolved.compact!
+      deduplicate_entities!(resolved)
       assign_serializer(resolved)
       yield resolved if block_given?
       @opts[:after_resolve]&.call(resolved)
-      resolved
+      @resolved_records = resolved
+    end
+
+    # Must run before sideloads assign, so every include path populates the
+    # canonical instance. The resource class in the key keeps two resources
+    # serving the same model from sharing an instance and a serializer.
+    def deduplicate_entities!(resolved)
+      return unless deduplicable?
+
+      # Nested maps are built once, a composite key meant a new array for every record
+      by_model = @query.entity_map.compute_if_absent(@resource.class) { Concurrent::Map.new }
+
+      resolved.map! do |record|
+        next record unless record.respond_to?(:id) && !record.id.nil?
+
+        by_id = by_model.compute_if_absent(record.class) { Concurrent::Map.new }
+        by_id.compute_if_absent(record.id) { record }
+      end
+    end
+
+    # A customized sideload can load a record another path would not, so it keeps its own instances.
+    def deduplicable?
+      return false unless @query.repeated_resource_classes.include?(@resource.class)
+
+      sideload = @opts[:sideload]
+      return true unless sideload
+
+      sideload.scope_proc.nil? &&
+        sideload.params_proc.nil? &&
+        !sideload.customized_base_scope? &&
+        sideload.primary_key == :id
+    end
+
+    # One sideload has nothing to run beside it, so the pool would hand the work
+    # to another thread and wait for it. A chain is one at every level. A
+    # polymorphic sideload counts as its children, which do run beside each other.
+    def overlapping_sideloads?
+      found = 0
+      each_applicable_sideload do |_, sideload, _|
+        found += sideload.respond_to?(:children) ? sideload.children.size : 1
+        return true if found > 1
+      end
+      false
     end
 
     def each_applicable_sideload
@@ -180,22 +226,40 @@ def each_applicable_sideload
         sideload = @resource.class.sideload(name)
         next if sideload.nil? || sideload.shared_remote?
 
-        yield sideload, sideload_query
+        yield name, sideload, sideload_query
+      end
+    end
+
+    # The write path resolves sideloads twice on one scope, which would double every proxy in the cache key.
+    def reset_captured_sideload_proxies
+      @resolved_sideload_proxies_lock.synchronize { @resolved_sideload_proxies.clear }
+    end
+
+    # A proxy built while resolving is already resolved all the way down.
+    def capture_sideload_proxy(name, proxy)
+      @resolved_sideload_proxies_lock.synchronize do
+        captured = (@resolved_sideload_proxies[name] ||= [])
+        captured << proxy unless proxy.nil? || proxy == []
       end
     end
 
     def future_resolve_sideloads(results)
       return Concurrent::Promises.fulfilled_future(nil, self.class.global_thread_pool_executor) if results == []
 
+      reset_captured_sideload_proxies
       sideload_promises = []
-      each_applicable_sideload do |sideload, sideload_query|
+      each_applicable_sideload do |name, sideload, sideload_query|
         promise = future_with_context(results, sideload_query, @resource) do |parent_results, future_query, parent_resource|
           Graphiti.config.before_sideload&.call(Graphiti.context)
-          sideload.future_resolve(parent_results, future_query, parent_resource)
+          sideload.future_resolve(parent_results, future_query, parent_resource) do |proxy|
+            capture_sideload_proxy(name, proxy)
+          end
         end
         sideload_promises << promise.flat
       end
 
+      return sideload_promises.first if sideload_promises.one?
+
       Concurrent::Promises.zip_futures_on(self.class.global_thread_pool_executor, *sideload_promises)
         .rescue_on(self.class.global_thread_pool_executor) do |*reasons|
           first_error = reasons.find { |r| r.is_a?(Exception) }
@@ -217,15 +281,21 @@ def future_with_context(*args)
           end
         end
 
+      current_attributes = current_attributes_snapshot
+
       Concurrent::Promises.future_on(
-        self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, *args
-      ) do |thread_id, thread_storage, fiber_storage, *args|
+        self.class.global_thread_pool_executor, Thread.current.object_id, thread_storage, fiber_storage, current_attributes, *args
+      ) do |thread_id, thread_storage, fiber_storage, current_attributes, *args|
         self.class.marking_pool_thread do
           wrap_in_rails_executor do
             with_thread_locals(thread_storage) do
               with_fiber_locals(fiber_storage) do
-                Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
-                  yield(*args)
+                with_current_attributes(current_attributes) do
+                  with_connection_pool_hint do
+                    Graphiti.broadcast(:global_thread_pool_task_run, self.class.global_thread_pool_stats) do
+                      yield(*args)
+                    end
+                  end
                 end
               end
             end
@@ -234,6 +304,45 @@ def future_with_context(*args)
       end
     end
 
+    def current_attributes_snapshot
+      return unless defined?(ActiveSupport::CurrentAttributes)
+
+      snapshot = {}
+      klasses = ActiveSupport::CurrentAttributes.subclasses
+      while (klass = klasses.shift)
+        klasses.concat(klass.subclasses)
+        attributes = klass.attributes
+        snapshot[klass] = attributes.dup if attributes.any?
+      end
+      snapshot
+    end
+
+    # Restored inside the Rails executor, whose entry hands the pool thread a
+    # fresh Current. Restoring through set scopes the values to the block.
+    def with_current_attributes(snapshot, &block)
+      return yield if snapshot.nil? || snapshot.empty?
+
+      klass, attributes = snapshot.first
+      rest = snapshot.except(klass)
+      klass.set(attributes) { with_current_attributes(rest, &block) }
+    end
+
+    # A timeout inside a sideload task nearly always means database.yml's pool
+    # was not sized for the sideload threads, and the bare error does not say so.
+    def with_connection_pool_hint
+      yield
+    rescue => error
+      raise unless defined?(ActiveRecord::ConnectionTimeoutError) && error.is_a?(ActiveRecord::ConnectionTimeoutError)
+
+      hinted = error.exception(<<~MSG.strip)
+        #{error.message}
+
+        Raised while resolving a sideload concurrently. Each concurrent sideload holds its own database connection, so `pool` in database.yml must be at least web threads + concurrency_max_threads (#{Graphiti.config.concurrency_max_threads}) + 1. See graphiti.dev/concepts/resources#concurrency-pool-sizing.
+      MSG
+      hinted.set_backtrace(error.backtrace)
+      raise hinted
+    end
+
     def with_thread_locals(thread_locals)
       new_thread_locals = []
       thread_locals.each do |key, value|
@@ -276,15 +385,19 @@ def wrap_in_rails_executor(&block)
 
     def sideload_resource_proxies
       @sideload_resource_proxies ||= begin
-        @object = @resource.before_resolve(@object, @query)
-        results = @resource.resolve(@object)
+        # Reached after the response resolved, where resolving again re-applies before_resolve to a mutated scope.
+        results = @resolved_records
+        if results.nil?
+          @object = @resource.before_resolve(@object, @query)
+          results = @resource.resolve(@object)
+        end
 
         [].tap do |proxies|
-          unless @query.sideloads.empty?
-            @query.sideloads.each_pair do |name, q|
-              sideload = @resource.class.sideload(name)
-              next if sideload.nil? || sideload.shared_remote?
-
+          each_applicable_sideload do |name, sideload, q|
+            captured = @resolved_sideload_proxies[name]
+            if captured
+              proxies.concat(captured)
+            else
               proxies << sideload.build_resource_proxy(results, q, parent_resource)
             end
           end
diff --git a/lib/graphiti/scoping/filter.rb b/lib/graphiti/scoping/filter.rb
index d86f897b..bb7bead1 100644
--- a/lib/graphiti/scoping/filter.rb
+++ b/lib/graphiti/scoping/filter.rb
@@ -62,7 +62,7 @@ def each_filter
             value = parse_string_value(filter.values[0], value)
           end
 
-          check_deny_empty_filters!(resource, filter, value)
+          check_blank_filters!(resource, filter, value)
           value = parse_string_null(filter.values[0], value)
           validate_singular(resource, filter, value)
           value = coerce_types(filter.values[0], param_name.to_sym, value)
@@ -82,7 +82,7 @@ def coerce_types(filter, name, value)
       if is_array
         @resource.typecast(name, value, :filterable)
       else
-        value = value.nil? || value.is_a?(Hash) ? [value] : Array(value)
+        value = (value.nil? || value.is_a?(Hash)) ? [value] : Array(value)
         value.map { |v| @resource.typecast(name, v, :filterable) }
       end
     end
@@ -100,7 +100,7 @@ def normalize_param(filter, param_value)
 
       param_value.map do |operator, value|
         if type == :hash
-          value, operator = \
+          value, operator =
             parse_hash_value(filter, param_value, value, operator)
         end
 
@@ -209,15 +209,15 @@ def parse_string_arrays(value, singular_filter)
     end
 
     def parse_string_null(filter, value)
-      return value unless filter[:allow_nil]
-      return value.map { |item| item == "null" ? nil : item } if value.is_a?(Array)
+      return value unless filter[:blanks] == :null
+      return value.map { |item| (item == "null") ? nil : item } if value.is_a?(Array)
       return if value == "null"
 
       value
     end
 
-    def check_deny_empty_filters!(resource, filter, value)
-      return unless filter.values[0][:deny_empty]
+    def check_blank_filters!(resource, filter, value)
+      return unless filter.values[0][:blanks] == :rejected
 
       if value.nil? || value.empty? || value == "null"
         raise Errors::InvalidFilterValue.new(resource, filter, "(empty)")
diff --git a/lib/graphiti/scoping/paginate.rb b/lib/graphiti/scoping/paginate.rb
index a0c2eab0..c378e8ad 100644
--- a/lib/graphiti/scoping/paginate.rb
+++ b/lib/graphiti/scoping/paginate.rb
@@ -4,9 +4,9 @@ class Scoping::Paginate < Scoping::Base
     PARAMS = [:number, :size, :offset, :before, :after]
 
     def apply
-      if size > resource.max_page_size
+      if size > resource.page_max_size
         raise Graphiti::Errors::UnsupportedPageSize
-          .new(size, resource.max_page_size)
+          .new(size, resource.page_max_size)
       elsif requested? && @opts[:sideload_parent_length].to_i > 1
         raise Graphiti::Errors::UnsupportedPagination
       else
@@ -61,7 +61,7 @@ def requested?
     end
 
     def page_param
-      @page_param ||= (query_hash[:page] || {})
+      @page_param ||= query_hash[:page] || {}
     end
 
     def offset
@@ -100,7 +100,7 @@ def number
     end
 
     def size
-      (page_param[:size] || resource.default_page_size || DEFAULT_PAGE_SIZE).to_i
+      (page_param[:size] || resource.page_default_size || DEFAULT_PAGE_SIZE).to_i
     end
   end
 end
diff --git a/lib/graphiti/scoping/sort.rb b/lib/graphiti/scoping/sort.rb
index 61e4c10d..7eb98364 100644
--- a/lib/graphiti/scoping/sort.rb
+++ b/lib/graphiti/scoping/sort.rb
@@ -75,7 +75,7 @@ def normalize(sort)
     end
 
     def sort_hash(attr)
-      value = attr[0] == "-" ? :desc : :asc
+      value = (attr[0] == "-") ? :desc : :asc
       key = attr.sub("-", "").to_sym
 
       {key => value}
diff --git a/lib/graphiti/serializer.rb b/lib/graphiti/serializer.rb
index de8d89ec..9611579c 100644
--- a/lib/graphiti/serializer.rb
+++ b/lib/graphiti/serializer.rb
@@ -13,10 +13,26 @@ class Serializer < JSONAPI::Serializable::Resource
     class_attribute :attributes_applied_via_resource
     class_attribute :extra_attributes_applied_via_resource
     class_attribute :relationship_condition_blocks
+    # Which sideload each generated relationship block was built for. A
+    # subclass inherits its parent's blocks, so this is how a redeclared
+    # relationship is told apart from one already applied, and from one the
+    # application wrote by hand.
+    class_attribute :relationship_sideloads
     self.attributes_applied_via_resource = []
     self.extra_attributes_applied_via_resource = []
     # See #requested_relationships
     self.relationship_condition_blocks ||= {}
+    self.relationship_sideloads ||= {}
+
+    # Keyed on the sideloads hash itself, which is reassigned whenever a
+    # relationship is applied, so the answer is recomputed exactly then.
+    def self.on_demand_links?
+      sideloads = relationship_sideloads
+      return @on_demand_links.last if @on_demand_links&.first.equal?(sideloads)
+
+      @on_demand_links = [sideloads, sideloads.each_value.any? { |sideload| sideload.link_mode == :on_demand }]
+      @on_demand_links.last
+    end
 
     def self.inherited(klass)
       super
@@ -53,7 +69,7 @@ def cursor
       starting_offset = 0
       page_param = @proxy.query.pagination
       if (page_number = page_param[:number])
-        page_size = page_param[:size] || @resource.default_page_size
+        page_size = page_param[:size] || @resource.page_default_size
         starting_offset = (page_number - 1) * page_size
       end
 
@@ -68,7 +84,7 @@ def cursor
 
     def as_jsonapi(kwargs = {})
       super(**kwargs).tap do |hash|
-        strip_relationships!(hash) if strip_relationships?
+        strip_relationships!(hash)
         add_links!(hash)
       end
     end
@@ -94,16 +110,29 @@ def add_links!(hash)
       hash[:links] = @resource.links(@object) if @resource.links?
     end
 
+    # The meta: {included: false} stub is jsonapi-serializable's filler, not JSON:API.
     def strip_relationships!(hash)
-      hash[:relationships]&.select! do |name, payload|
-        payload.key?(:data)
+      placeholders = relationship_placeholders?
+      return if placeholders && !strip_on_demand_relationships?
+
+      hash[:relationships]&.reject! do |name, payload|
+        next false if payload.key?(:data) || payload.key?(:links)
+        next true unless placeholders
+
+        self.class.relationship_sideloads[name]&.link_mode == :on_demand
       end
     end
 
-    def strip_relationships?
-      return false unless Graphiti.config.links_on_demand
-      params = Graphiti.context[:object]&.params || {}
-      [false, nil, "false"].include?(params[:links])
+    # A remote resource exposes a stand-in object rather than a Resource.
+    def relationship_placeholders?
+      resource_class = @resource.class
+      return Resource.relationship_placeholders unless resource_class.respond_to?(:relationship_placeholders)
+
+      resource_class.relationship_placeholders
+    end
+
+    def strip_on_demand_relationships?
+      self.class.on_demand_links? && !@proxy&.query&.render_link?(:on_demand)
     end
   end
 end
diff --git a/lib/graphiti/sideload.rb b/lib/graphiti/sideload.rb
index c1beb73f..ab3d9d6b 100644
--- a/lib/graphiti/sideload.rb
+++ b/lib/graphiti/sideload.rb
@@ -20,6 +20,7 @@ class Sideload
     def initialize(name, opts)
       @name = name
       validate_options!(opts)
+      translate_deprecated_options!(opts)
       @parent_resource_class = opts[:parent_resource]
       @resource_class_name = opts[:resource]
       @primary_key = opts[:primary_key]
@@ -30,6 +31,9 @@ def initialize(name, opts)
       @writable = opts[:writable]
       @as = opts[:as]
       @link = opts[:link]
+      unless @link.nil? || Resource::LINK_MODES.include?(@link)
+        raise Errors::InvalidLinkRendering.new(@parent_resource_class, :"#{name} link", @link)
+      end
       @single = opts[:single]
       @remote = opts[:remote]
       apply_belongs_to_many_filter if type == :many_to_many
@@ -42,7 +46,7 @@ def initialize(name, opts)
       @group_name = opts[:group_name]
       @polymorphic_child = opts[:polymorphic_child]
       @parent = opts[:parent]
-      @always_include_resource_ids = opts[:always_include_resource_ids]
+      @render_resource_ids = opts[:resource_ids]
 
       if polymorphic_child?
         parent.resource.polymorphic << resource_class
@@ -57,6 +61,28 @@ def self.scope(&blk)
       self.scope_proc = blk
     end
 
+    ASSIGNING_QUERY = :__graphiti_assigning_query
+    private_constant :ASSIGNING_QUERY
+
+    def self.assigning_node(query)
+      return yield if query.nil?
+
+      previous = Fiber[ASSIGNING_QUERY]
+      Fiber[ASSIGNING_QUERY] = query
+      yield
+    ensure
+      Fiber[ASSIGNING_QUERY] = previous unless query.nil?
+    end
+
+    def self.current_assigning_query
+      Fiber[ASSIGNING_QUERY]
+    end
+
+    def self.assigned?(query, record, association_name)
+      owners = query.association_owners[association_name]
+      !owners.nil? && owners.key?(record.object_id)
+    end
+
     def self.assign(&blk)
       self.assign_proc = blk
     end
@@ -83,7 +109,8 @@ def create_remote_resource
         self.adapter = Graphiti::Adapters::GraphitiAPI
         self.model = OpenStruct
         self.remote = remote_url
-        self.validate_endpoints = false
+        self.validate_requests = false
+        self.validate_links = false
       }
       name = "#{parent_resource_class.name}.#{@name}.remote"
       klass.class_eval("def self.name;'#{name}';end", __FILE__, __LINE__)
@@ -110,6 +137,31 @@ def guarded?
       dynamic_flag?(@readable) || dynamic_flag?(@writable)
     end
 
+    def readable_guarded?
+      dynamic_flag?(@readable)
+    end
+
+    def readable_guard_name
+      @readable.to_sym if @readable.is_a?(Symbol) || @readable.is_a?(String)
+    end
+
+    def non_default_options
+      options = {}
+      options[:as] = association_name if @as
+      options[:primary_key] = primary_key unless primary_key == :id
+      options[:single] = true if single?
+      options[:remote] = @remote if remote?
+      options[:link] = @link unless @link.nil?
+      options[:readable] = @readable unless @readable.nil? || @readable == true
+      options[:writable] = @writable unless @writable.nil? || @writable == true
+      options[:resource_ids] = @render_resource_ids unless @render_resource_ids.nil?
+      options
+    end
+
+    def customized_base_scope?
+      !!@base_scope
+    end
+
     def single?
       !!@single
     end
@@ -122,18 +174,38 @@ def polymorphic_has_many?
       !!@polymorphic_as
     end
 
-    def always_include_resource_ids?
-      !!@always_include_resource_ids
+    # Every check behind the blocker is static sideload configuration, and this
+    # is asked once per rendered record per relationship.
+    def resource_ids_from_foreign_key?
+      return @resource_ids_from_foreign_key unless @resource_ids_from_foreign_key.nil?
+
+      @resource_ids_from_foreign_key = resource_ids_blocker.nil?
     end
 
-    def link?
-      return true if link_proc
+    def resource_ids_blocker
+      :no_foreign_key_on_parent
+    end
 
-      if @link.nil?
-        !!@parent_resource_class.autolink
-      else
-        !!@link
-      end
+    def render_resource_ids?
+      return !!@render_resource_ids unless @render_resource_ids.nil?
+
+      default_render_resource_ids?
+    end
+
+    def default_render_resource_ids?
+      false
+    end
+
+    # A custom link block means the author wants the link, so a false default does not silence it.
+    def link_mode
+      return @link unless @link.nil?
+
+      default = @parent_resource_class.relationship_links
+      (link_proc && default == false) ? true : default
+    end
+
+    def link?
+      link_mode != false
     end
 
     def link_filter(parents)
@@ -239,14 +311,20 @@ def build_resource_proxy(parents, query, graph_parent)
       proxy
     end
 
-    def load(parents, query, graph_parent)
+    def load(parents, query, graph_parent, &proxy_block)
       if Scope.resolve_synchronously?
-        build_resource_proxy(parents, query, graph_parent).to_a
+        sync_load(parents, query, graph_parent, &proxy_block)
       else
-        future_load(parents, query, graph_parent).value!
+        future_load(parents, query, graph_parent, &proxy_block).value!
       end
     end
 
+    def sync_load(parents, query, graph_parent, &proxy_block)
+      proxy = build_resource_proxy(parents, query, graph_parent)
+      proxy_block&.call(proxy)
+      proxy.to_a
+    end
+
     # Override in subclass
     def infer_foreign_key
       model = parent_resource_class.model
@@ -294,23 +372,38 @@ def assign(parents, children)
       children.replace(associated) if track_associated
     end
 
-    def resolve(parents, query, graph_parent)
+    def resolve(parents, query, graph_parent, &proxy_block)
       if Scope.resolve_synchronously?
-        sync_resolve(parents, query, graph_parent)
+        sync_resolve(parents, query, graph_parent, &proxy_block)
+      else
+        future_resolve(parents, query, graph_parent, &proxy_block).value!
+      end
+    end
+
+    # Called by a scope that already decided to stay inline, so it must not consult the pool again.
+    # A scope_proc builds a Scope rather than a proxy, and that nested scope decides for itself.
+    def sync_resolve(parents, query, graph_parent, &proxy_block)
+      assert_singular!(parents)
+
+      if self.class.scope_proc
+        build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
+          fire_assign(parents, sideload_results, query)
+        end
       else
-        future_resolve(parents, query, graph_parent).value!
+        sync_load(parents, query, graph_parent, &proxy_block)
       end
     end
 
-    def future_resolve(parents, query, graph_parent)
+    # A scope_proc builds a Scope rather than a proxy, and a Scope's cache key omits what a proxy's carries.
+    def future_resolve(parents, query, graph_parent, &proxy_block)
       assert_singular!(parents)
 
       if self.class.scope_proc
         build_sideload_scope(parents, query, graph_parent).future_resolve do |sideload_results|
-          fire_assign(parents, sideload_results)
+          fire_assign(parents, sideload_results, query)
         end
       else
-        future_load(parents, query, graph_parent)
+        future_load(parents, query, graph_parent, &proxy_block)
       end
     end
 
@@ -342,10 +435,14 @@ def fire_hooks!(parent, objects, method)
     end
 
     def associate_all(parent, children)
+      return unless claim_association(parent)
+
       parent_resource.associate_all(parent, children, association_name, type)
     end
 
     def associate(parent, child)
+      return unless claim_association(parent)
+
       parent_resource.associate(parent, child, association_name, type)
     end
 
@@ -374,18 +471,6 @@ def resource_class_loaded?
 
     private
 
-    def sync_resolve(parents, query, graph_parent)
-      assert_singular!(parents)
-
-      if self.class.scope_proc
-        build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
-          fire_assign(parents, sideload_results)
-        end
-      else
-        load(parents, query, graph_parent)
-      end
-    end
-
     def assert_singular!(parents)
       if single? && parents.length > 1
         raise Errors::SingularSideload.new(self, parents.length)
@@ -402,8 +487,9 @@ def build_sideload_scope(parents, query, graph_parent)
         default_paginate: false
     end
 
-    def future_load(parents, query, graph_parent)
+    def future_load(parents, query, graph_parent, &proxy_block)
       proxy = build_resource_proxy(parents, query, graph_parent)
+      proxy_block&.call(proxy)
       proxy.respond_to?(:future_resolve_data) ? proxy.future_resolve_data : Concurrent::Promises.fulfilled_future(proxy)
     end
 
@@ -416,6 +502,18 @@ def blank_query?(params)
       false
     end
 
+    def translate_deprecated_options!(opts)
+      return unless opts.key?(:always_include_resource_ids)
+
+      Graphiti::DEPRECATOR.deprecation_warning(
+        :always_include_resource_ids,
+        "Use :resource_ids instead (#{opts[:parent_resource]&.name}##{@name})"
+      )
+
+      value = opts.delete(:always_include_resource_ids)
+      opts[:resource_ids] = value unless opts.key?(:resource_ids)
+    end
+
     def validate_options!(opts)
       if opts[:remote]
         if opts[:resource]
@@ -434,7 +532,7 @@ def load_options(parents, query)
         opts[:sideload_parent_length] = parents.length
         opts[:query] = query
         opts[:after_resolve] = ->(results) {
-          fire_assign(parents, results)
+          fire_assign(parents, results, query)
         }
       end
     end
@@ -447,16 +545,28 @@ def fire_assign_each(parent, children)
       end
     end
 
-    def fire_assign(parents, children)
-      with_error_handling Errors::SideloadAssignError do
-        if self.class.assign_proc
-          instance_exec(parents, children, &self.class.assign_proc)
-        else
-          assign(parents, children)
+    def fire_assign(parents, children, query = nil)
+      self.class.assigning_node(query) do
+        with_error_handling Errors::SideloadAssignError do
+          if self.class.assign_proc
+            instance_exec(parents, children, &self.class.assign_proc)
+          else
+            assign(parents, children)
+          end
         end
       end
     end
 
+    # A record can be shared by two nodes of the include tree, and a node that narrows
+    # differently returns different rows, so the first node to populate an association owns it.
+    def claim_association(parent)
+      query = self.class.current_assigning_query
+      return true if query.nil?
+
+      owners = query.association_owners.compute_if_absent(association_name) { Concurrent::Map.new }
+      owners.compute_if_absent(parent.object_id) { query.hash } == query.hash
+    end
+
     def with_error_handling(error_class)
       begin
         result = yield
diff --git a/lib/graphiti/sideload/belongs_to.rb b/lib/graphiti/sideload/belongs_to.rb
index 8eb4e458..5332f7e5 100644
--- a/lib/graphiti/sideload/belongs_to.rb
+++ b/lib/graphiti/sideload/belongs_to.rb
@@ -1,13 +1,39 @@
 class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
-  def initialize(name, opts)
-    opts = {always_include_resource_ids: false}.merge(opts)
-    super(name, opts)
-  end
-
   def type
     :belongs_to
   end
 
+  def default_render_resource_ids?
+    case parent_resource_class&.belongs_to_resource_ids_by_default
+    when :always then renderable_at_all?
+    when :never then false
+    else resource_ids_from_foreign_key?
+    end
+  end
+
+  def renderable_at_all?
+    readable_guarded? || readable?
+  end
+
+  def resource_ids_blocker
+    return :unreadable unless renderable_at_all?
+    return :custom_primary_key unless foreign_key_is_related_id?
+    return :polymorphic_child if polymorphic_child?
+    return :scope_block if self.class.scope_proc
+    return :params_block if self.class.params_proc
+    return :base_scope if @base_scope
+    return :remote if remote?
+    return :polymorphic_resource if resource.class.polymorphic.present?
+
+    nil
+  end
+
+  # base_filter matches the foreign key against primary_key, so a custom
+  # primary_key means the key holds that column's value, not the related id.
+  def foreign_key_is_related_id?
+    primary_key == :id
+  end
+
   def load_params(parents, query)
     query.hash.tap do |hash|
       hash[:filter] ||= {}
diff --git a/lib/graphiti/sideload/has_many.rb b/lib/graphiti/sideload/has_many.rb
index 0a460297..1b485bc7 100644
--- a/lib/graphiti/sideload/has_many.rb
+++ b/lib/graphiti/sideload/has_many.rb
@@ -2,7 +2,7 @@ class Graphiti::Sideload::HasMany < Graphiti::Sideload
   def initialize(name, opts)
     @inverse_filter = opts[:inverse_filter]
 
-    super(name, opts)
+    super
   end
 
   def type
diff --git a/lib/graphiti/sideload/polymorphic_belongs_to.rb b/lib/graphiti/sideload/polymorphic_belongs_to.rb
index bb1e0412..00db5e24 100644
--- a/lib/graphiti/sideload/polymorphic_belongs_to.rb
+++ b/lib/graphiti/sideload/polymorphic_belongs_to.rb
@@ -107,30 +107,39 @@ def child_for_type!(type)
     end
   end
 
-  def resolve(parents, query, graph_parent)
+  def resolve(parents, query, graph_parent, &proxy_block)
     if ::Graphiti::Scope.resolve_synchronously?
-      sync_resolve(parents, query, graph_parent)
+      sync_resolve(parents, query, graph_parent, &proxy_block)
     else
-      future_resolve(parents, query, graph_parent).value!
+      future_resolve(parents, query, graph_parent, &proxy_block).value!
     end
   end
 
-  def future_resolve(parents, query, graph_parent)
+  def future_resolve(parents, query, graph_parent, &proxy_block)
     promises = []
     each_resolvable_group(parents, query) do |child, group, child_query|
-      promises << child.future_resolve(group, child_query, graph_parent)
+      promises << child.future_resolve(group, child_query, graph_parent, &proxy_block)
     end
-    Concurrent::Promises.zip(*promises)
-  end
+    return promises.first if promises.one?
 
-  private
+    executor = ::Graphiti::Scope.global_thread_pool_executor
+    Concurrent::Promises.zip_futures_on(executor, *promises)
+      .rescue_on(executor) do |*reasons|
+        first_error = reasons.find { |reason| reason.is_a?(Exception) }
+        raise first_error
+      end
+  end
 
-  def sync_resolve(parents, query, graph_parent)
+  def sync_resolve(parents, query, graph_parent, &proxy_block)
     each_resolvable_group(parents, query) do |child, group, child_query|
-      child.resolve(group, child_query, graph_parent)
+      child.sync_resolve(group, child_query, graph_parent, &proxy_block)
     end
   end
 
+  private
+
+  # Group parents by their polymorphic type and yield each group's child
+  # sideload alongside a query pruned to the sideloads that child supports.
   def each_resolvable_group(parents, query)
     parents.group_by(&grouper.field_name).each_pair do |group_name, group|
       next if group_name.nil? || grouper.ignore?(group_name)
diff --git a/lib/graphiti/spec_helpers.rb b/lib/graphiti/spec_helpers.rb
new file mode 100644
index 00000000..56e2a79d
--- /dev/null
+++ b/lib/graphiti/spec_helpers.rb
@@ -0,0 +1,59 @@
+# Merged in from the standalone graphiti_spec_helpers gem, retired as of
+# Graphiti 2.0. The GraphitiSpecHelpers namespace and the
+# "graphiti_spec_helpers/rspec" require path are kept verbatim so existing spec
+# suites need no changes beyond dropping the gem from their Gemfile.
+require "json"
+require "active_support/core_ext/string"
+require "active_support/core_ext/hash"
+require "graphiti"
+
+require "graphiti/spec_helpers/helpers"
+require "graphiti/spec_helpers/node"
+require "graphiti/spec_helpers/errors_proxy"
+require "graphiti/spec_helpers/errors"
+require "graphiti/spec_helpers/matchers"
+
+module Graphiti
+  module SpecHelpers
+    def self.included(klass)
+      klass.send(:include, Helpers)
+    end
+
+    class TestRunner < ::Graphiti::Runner
+      def current_user
+        nil
+      end
+    end
+
+    module Sugar
+      def self.included(base)
+        Graphiti::DEPRECATOR.warn("Graphiti::SpecHelpers::Sugar is deprecated. Call jsonapi_data, jsonapi_included, jsonapi_errors, json_date and json_datetime directly.")
+        super
+      end
+
+      def d
+        jsonapi_data
+      end
+
+      def included(type = nil)
+        jsonapi_included(type)
+      end
+
+      def errors
+        jsonapi_errors
+      end
+
+      def dt(*args)
+        json_datetime(*args)
+      end
+
+      def datetime(*args)
+        json_datetime(*args)
+      end
+
+      def date(*args)
+        json_date(*args)
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/spec_helpers/errors.rb b/lib/graphiti/spec_helpers/errors.rb
new file mode 100644
index 00000000..e428117c
--- /dev/null
+++ b/lib/graphiti/spec_helpers/errors.rb
@@ -0,0 +1,73 @@
+module Graphiti
+  module SpecHelpers
+    module Errors
+      class Base < StandardError; end
+
+      class LinksNotFound < Base
+        def initialize(name)
+          @name = name
+        end
+
+        def message
+          "Relationship with name '#{@name}' has no links!"
+        end
+      end
+
+      class NoAttribute < Base
+        def initialize(name)
+          @name = name
+        end
+
+        def message
+          "No attribute '#{@name}' in JSON response node!"
+        end
+      end
+
+      class SideloadNotFound < Base
+        def initialize(name)
+          @name = name
+        end
+
+        def message
+          "Relationship with name '#{@name}' not found!"
+        end
+      end
+
+      class NoResponse < Base
+        def message
+          "Cannot parse response - missing #response.body!"
+        end
+      end
+
+      class NoData < Base
+        def initialize(payload)
+          @payload = payload
+        end
+
+        def message
+          "Payload did not contain 'data'! Payload was:\n\n#{JSON.pretty_generate(@payload)}"
+        end
+      end
+
+      class NoSideloads < Base
+        def initialize(payload)
+          @payload = payload
+        end
+
+        def message
+          "Tried to find sideload, but the payload did not contain 'included'! Payload was:\n\n#{JSON.pretty_generate(@payload)}"
+        end
+      end
+
+      class NoMeta < Base
+        def initialize(payload)
+          @payload = payload
+        end
+
+        def message
+          "Tried to find metadata, but the payload did not contain 'meta'! Payload was:\n\n#{JSON.pretty_generate(@payload)}"
+        end
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/spec_helpers/errors_proxy.rb b/lib/graphiti/spec_helpers/errors_proxy.rb
new file mode 100644
index 00000000..f56e95b7
--- /dev/null
+++ b/lib/graphiti/spec_helpers/errors_proxy.rb
@@ -0,0 +1,75 @@
+module Graphiti
+  module SpecHelpers
+    class ErrorsProxy
+      class Error
+        attr_reader :json
+
+        def initialize(json)
+          @json = json
+        end
+
+        def attribute
+          @json[:meta][:attribute]
+        end
+
+        # TODO: move to top-level code in errorable
+        def code
+          @json[:meta][:code]
+        end
+
+        def message
+          @json[:meta][:message]
+        end
+
+        def title
+          @json[:title]
+        end
+
+        def detail
+          @json[:detail]
+        end
+
+        def status
+          @json[:status]
+        end
+      end
+
+      include Enumerable
+
+      def initialize(array)
+        @errors = array.map { |e| Error.new(e) }
+      end
+
+      def [](key)
+        @errors[key]
+      end
+
+      def each(&blk)
+        @errors.each(&blk)
+      end
+
+      def length
+        count
+      end
+
+      def to_h
+        {}.tap do |hash|
+          @errors.each do |e|
+            hash[e.attribute] = e.message
+          end
+        end
+      end
+
+      def method_missing(id, *args, &blk)
+        matches = @errors.select { |e| e.attribute.to_sym == id }
+        (matches.length == 1) ? matches[0] : matches
+      end
+
+      # Any name is answered: an attribute with no matching error yields an empty
+      # array rather than raising NoMethodError.
+      def respond_to_missing?(id, include_private = false)
+        true
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/spec_helpers/helpers.rb b/lib/graphiti/spec_helpers/helpers.rb
new file mode 100644
index 00000000..6e9de6ab
--- /dev/null
+++ b/lib/graphiti/spec_helpers/helpers.rb
@@ -0,0 +1,107 @@
+module Graphiti
+  module SpecHelpers
+    module Helpers
+      extend ActiveSupport::Concern
+
+      def json
+        if response&.body
+          JSON.parse(response.body).with_indifferent_access
+        else
+          raise Errors::NoResponse.new
+        end
+      end
+
+      def jsonapi_data
+        @jsonapi_data ||= if _jsonapi_data.is_a?(Hash)
+          node(from: _jsonapi_data)
+        else
+          _jsonapi_data.map { |datum| node(from: datum) }
+        end
+      end
+
+      def jsonapi_included(type = nil)
+        variable = :"@jsonapi_included#{type}"
+        memo = instance_variable_get(variable)
+        return memo if memo
+
+        nodes = _jsonapi_included.map { |i| node(from: i) }
+        if type
+          nodes.select! { |n| n.jsonapi_type == type.to_s }
+        end
+        instance_variable_set(variable, nodes)
+        nodes
+      end
+
+      def jsonapi_errors
+        @jsonapi_errors ||= ErrorsProxy.new(json["errors"] || [])
+      end
+
+      def jsonapi_meta
+        @jsonapi_errors = json["meta"] || raise(Errors::NoMeta.new(json))
+      end
+
+      def jsonapi_headers
+        media_type = "application/vnd.api+json"
+        {
+          "CONTENT_TYPE" => media_type,
+          "HTTP_ACCEPT" => media_type
+        }
+      end
+
+      def jsonapi_get(url, params: {}, headers: {})
+        get url_for(url), params: params, headers: jsonapi_headers.merge(headers)
+      end
+
+      def jsonapi_post(url, payload, headers: {})
+        post url_for(url), params: payload.to_json, headers: jsonapi_headers.merge(headers)
+      end
+
+      def jsonapi_put(url, payload, headers: {})
+        put url_for(url), params: payload.to_json, headers: jsonapi_headers.merge(headers)
+      end
+
+      def jsonapi_patch(url, payload, headers: {})
+        patch url_for(url), params: payload.to_json, headers: jsonapi_headers.merge(headers)
+      end
+
+      def jsonapi_delete(url, headers: {})
+        delete url_for(url), headers: jsonapi_headers.merge(headers)
+      end
+
+      def json_datetime(value)
+        Graphiti::Types[:datetime][:read][value].as_json
+      end
+
+      def json_date(value)
+        Graphiti::Types[:date][:read][value].as_json
+      end
+
+      # @api private
+      def node(from: nil)
+        from = json if from.nil?
+        data = from.has_key?("data") ? from["data"] : from
+        hash = {}
+        hash["id"] = data["id"]
+        hash["jsonapi_type"] = data["type"]
+        hash.merge!(data["attributes"]) if data.has_key?("attributes")
+        Node.new(hash, data["relationships"], self)
+      end
+
+      private
+
+      # @api private
+      def _jsonapi_data
+        json["data"] || raise(Errors::NoData.new(json))
+      end
+
+      # @api private
+      def _jsonapi_included
+        if json.has_key?("included")
+          json["included"]
+        else
+          raise Errors::NoSideloads.new(json)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/spec_helpers/matchers.rb b/lib/graphiti/spec_helpers/matchers.rb
new file mode 100644
index 00000000..4061c1e8
--- /dev/null
+++ b/lib/graphiti/spec_helpers/matchers.rb
@@ -0,0 +1,197 @@
+module Graphiti
+  module SpecHelpers
+    # Assertions against a resource's DSL. The schema specs cannot cover
+    # adapter-dependent options (primary key, foreign key, and so on) because
+    # the schema does not carry them; these read the resource config directly.
+    module Matchers
+      class BaseMatcher
+        GRAPHITI_OPTS = [].freeze
+        GRAPHITI_CONFIG_KEY = ""
+        EXPECTED_ACTION = ""
+
+        def description
+          "#{self.class::EXPECTED_ACTION} #{target}"
+        end
+
+        def failure_message
+          "expected that #{resource.class} would #{self.class::EXPECTED_ACTION} #{target}\n#{@opt_failures.join("\n")}"
+        end
+
+        def failure_message_when_negated
+          "expected that #{resource.class} would not #{self.class::EXPECTED_ACTION} #{target}"
+        end
+
+        def opt_failure_message(opt, expected, actual)
+          "expected that #{opt} would be #{expected}, was #{actual}"
+        end
+
+        def does_not_match?(resource)
+          !matches?(resource)
+        end
+
+        def matches?(resource)
+          @resource = resource
+
+          expected? && expected_opts?
+        end
+
+        def with_options(opts)
+          @opts = opts
+          self
+        end
+
+        private
+
+        def actual_opts
+          self.class::GRAPHITI_OPTS & opts.keys
+        end
+
+        def config
+          @config ||= resource.class.config[self.class::GRAPHITI_CONFIG_KEY][target]
+        end
+
+        def expected_opts?
+          return false unless config
+
+          actual_opts.map { |opt| assert_opt(opt) }.all?(true)
+        end
+      end
+
+      class RelationMatcher < BaseMatcher
+        GRAPHITI_OPTS = %i[primary_key foreign_key resource readable writable link single].freeze
+        SIDELOAD_METHODS = {resource: :resource_class, readable: :readable?, writable: :writable?, single: :single?}.freeze
+        GRAPHITI_CONFIG_KEY = :sideloads
+        EXPECTED_ACTION = ""
+
+        def initialize(target)
+          @target = target
+          @opts = {}
+          @opt_failures = []
+        end
+
+        private
+
+        attr_reader :target, :opts, :resource
+
+        def assert_opt(opt)
+          asserted_opt = SIDELOAD_METHODS.fetch(opt, opt)
+          return true if config.send(asserted_opt) == opts[opt]
+
+          @opt_failures << opt_failure_message(opt, opts[opt], config.send(asserted_opt))
+          false
+        end
+
+        def expected?
+          config && config.type == relation_name
+        end
+
+        # BelongsToMatcher -> :belongs_to, matching Sideload#type.
+        def relation_name
+          self.class.name.demodulize.gsub("Matcher", "").underscore.to_sym
+        end
+      end
+
+      class BelongsToMatcher < RelationMatcher
+        EXPECTED_ACTION = "belong to"
+      end
+
+      class HasManyMatcher < RelationMatcher
+        EXPECTED_ACTION = "have many"
+      end
+
+      class HasOneMatcher < RelationMatcher
+        EXPECTED_ACTION = "have one"
+      end
+
+      class ResourceDSLMatcher < BaseMatcher
+        def initialize(target, type)
+          @target = target
+          @type = type
+          @opts = {}
+          @opt_failures = []
+        end
+
+        private
+
+        attr_reader :target, :type, :opts, :resource
+
+        def expected?
+          config && assert_type
+        end
+
+        def assert_type
+          return true if config[:type] == type
+
+          @opt_failures << opt_failure_message("type", type, config[:type])
+          false
+        end
+
+        def assert_opt(opt)
+          return true if config[opt] == opts[opt]
+
+          @opt_failures << opt_failure_message(opt, opts[opt], config[opt])
+          false
+        end
+      end
+
+      class ExposeAttributeMatcher < ResourceDSLMatcher
+        GRAPHITI_OPTS = %i[writable readable sortable filterable].freeze
+        GRAPHITI_CONFIG_KEY = :attributes
+        EXPECTED_ACTION = "expose"
+      end
+
+      class FilterAttributeMatcher < ResourceDSLMatcher
+        GRAPHITI_OPTS = %i[allow deny single required blanks].freeze
+        GRAPHITI_CONFIG_KEY = :filters
+        EXPECTED_ACTION = "filter"
+      end
+
+      # @param [Symbol] relation
+      #
+      # @example expect(subject).to belong_to_resource(:user)
+      # @example expect(subject).to belong_to_resource(:user).with_options(foreign_key: :user_id, resource: UserResource)
+      # @example expect(subject).not_to belong_to_resource(:user)
+      def belong_to_resource(relation)
+        BelongsToMatcher.new(relation)
+      end
+
+      # @param [Symbol] relation
+      #
+      # @example expect(subject).to have_many_resources(:posts)
+      # @example expect(subject).to have_many_resources(:posts).with_options(foreign_key: :post_id, resource: PostResource)
+      # @example expect(subject).not_to have_many_resources(:posts)
+      def have_many_resources(relation)
+        HasManyMatcher.new(relation)
+      end
+
+      # @param [Symbol] relation
+      #
+      # @example expect(subject).to have_one_resource(:post)
+      # @example expect(subject).to have_one_resource(:post).with_options(foreign_key: :post_id, resource: PostResource)
+      # @example expect(subject).not_to have_one_resource(:post)
+      def have_one_resource(relation)
+        HasOneMatcher.new(relation)
+      end
+
+      # @param [Symbol] attribute
+      # @param [Symbol] type
+      #
+      # @example expect(subject).to expose_attribute(:name, :string)
+      # @example expect(subject).to expose_attribute(:name, :string).with_options(writable: false)
+      # @example expect(subject).not_to expose_attribute(:name, :string)
+      def expose_attribute(attribute, type)
+        ExposeAttributeMatcher.new(attribute, type)
+      end
+
+      # @param [Symbol] attribute
+      # @param [Symbol] type
+      #
+      # @example expect(subject).to filter_attribute(:name, :string)
+      # @example expect(subject).to filter_attribute(:name, :string).with_options(blanks: :null)
+      # @example expect(subject).not_to filter_attribute(:name, :string)
+      def filter_attribute(attribute, type)
+        FilterAttributeMatcher.new(attribute, type)
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/spec_helpers/node.rb b/lib/graphiti/spec_helpers/node.rb
new file mode 100644
index 00000000..5abaedd7
--- /dev/null
+++ b/lib/graphiti/spec_helpers/node.rb
@@ -0,0 +1,88 @@
+module Graphiti
+  module SpecHelpers
+    class Node
+      attr_reader :attributes, :relationships
+
+      def initialize(attributes, relationships, context)
+        @attributes = attributes.with_indifferent_access
+        @relationships = relationships.with_indifferent_access if relationships
+        @context = context
+      end
+
+      def id
+        Integer(rawid) # Only convert if using integer ids
+      rescue ArgumentError
+        rawid
+      end
+
+      def rawid
+        @attributes["id"]
+      end
+
+      def jsonapi_type
+        @attributes["jsonapi_type"]
+      end
+
+      def has_key?(key)
+        @attributes.has_key?(key)
+      end
+      alias_method :key?, :has_key?
+
+      def [](key)
+        @attributes[key] || @attributes[key.to_s]
+      end
+
+      def []=(key, val)
+        @attributes[key] = val
+      end
+
+      alias_method :to_hash, :attributes
+
+      def method_missing(id, *args, &blk)
+        if @attributes.has_key?(id)
+          @attributes[id]
+        else
+          raise Errors::NoAttribute.new(id)
+        end
+      end
+
+      def respond_to_missing?(id, include_private = false)
+        @attributes.has_key?(id) || super
+      end
+
+      def link(relationship_name, name)
+        if @relationships.has_key?(relationship_name)
+          links = @relationships[relationship_name][:links]
+          raise Errors::LinksNotFound.new(relationship_name) unless links
+          links[name]
+        else
+          raise Errors::SideloadNotFound.new(relationship_name)
+        end
+      end
+
+      def sideload(relationship_name)
+        unless @relationships.has_key?(relationship_name)
+          raise Errors::SideloadNotFound.new(relationship_name)
+        end
+        rel = @relationships[relationship_name]
+        rel = rel[:data]
+        return if rel.nil?
+        if rel.is_a?(Hash)
+          include_for(rel[:type], rel[:id])
+        else
+          rel.map { |r| include_for(r[:type], r[:id]) }
+        end
+      end
+      alias_method :sideloads, :sideload
+
+      private
+
+      def include_for(type, id)
+        data = @context.json["included"].find do |i|
+          i["type"] == type && i["id"] == id
+        end
+        @context.node(from: data)
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/spec_helpers/rspec.rb b/lib/graphiti/spec_helpers/rspec.rb
new file mode 100644
index 00000000..36047eda
--- /dev/null
+++ b/lib/graphiti/spec_helpers/rspec.rb
@@ -0,0 +1,143 @@
+require "rspec/core"
+require "graphiti/spec_helpers"
+
+module Graphiti
+  module SpecHelpers
+    class ContextProxy < OpenStruct
+      def initialize(proxied, *args)
+        @__proxied = proxied
+        super(*args)
+      end
+
+      # variables defined in the rspec context should remain lazy
+      def current_user
+        super || __proxied_current_user
+      end
+
+      def params
+        super || __proxied_params
+      end
+
+      private
+
+      def __proxied_current_user
+        @__proxied.current_user if @__proxied.respond_to?(:current_user)
+      end
+
+      def __proxied_params
+        @__proxied.params if @__proxied.respond_to?(:params)
+      end
+    end
+  end
+end
+
+# Registered under both names. The graphiti-prefixed name is canonical. The bare
+# one is kept for suites that include it explicitly, and carries no `type:`
+# metadata so a `type: :resource` group does not pick the context up twice.
+resource_testing = proc do
+  let(:resource) { described_class }
+  let(:params) { {} }
+
+  around do |e|
+    original_requests = Graphiti::Resource.validate_requests
+    original_links = Graphiti::Resource.validate_links
+    Graphiti::Resource.validate_requests = false
+    Graphiti::Resource.validate_links = false
+
+    Graphiti.with_context graphiti_context do
+      e.run
+    end
+  ensure
+    Graphiti::Resource.validate_requests = original_requests
+    Graphiti::Resource.validate_links = original_links
+  end
+
+  def graphiti_context
+    @graphiti_context ||= Graphiti::SpecHelpers::ContextProxy.new(self)
+  end
+
+  # If you need to set context:
+  #
+  # Graphiti.with_context my_context, {} do
+  #   render
+  # end
+  def render(runtime_options = {})
+    json = proxy.to_jsonapi(runtime_options)
+    response.body = json
+    json
+  end
+
+  def proxy
+    @proxy ||= begin
+      args = [params]
+      args << base_scope if defined?(base_scope)
+      resource.all(*args)
+    end
+  end
+
+  def records
+    proxy.data
+  end
+
+  def response
+    @response ||= OpenStruct.new
+  end
+end
+
+remote_api = proc do
+  # Fake request headers
+  around do |e|
+    ctx = OpenStruct.new \
+      request: OpenStruct.new(headers: OpenStruct.new)
+    Graphiti.with_context(ctx) { e.run }
+  end
+
+  def mock_api(url, json, call_count = 1)
+    api_response = double(body: json.to_json)
+    expect(Faraday).to receive(:get)
+      .with(url, anything, anything)
+      .exactly(call_count).times
+      .and_return(api_response)
+  end
+end
+
+# Was a top-level constant, and reopening it to add context methods was a way
+# suites customised graphiti_context. Remove in 3.0.
+GraphitiContextProxy = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
+  "GraphitiContextProxy",
+  "Graphiti::SpecHelpers::ContextProxy",
+  Graphiti::DEPRECATOR
+)
+
+::RSpec.shared_context("graphiti resource testing", type: :resource, &resource_testing)
+::RSpec.shared_context("resource testing", &resource_testing)
+
+::RSpec.shared_context("graphiti remote api", &remote_api)
+::RSpec.shared_context("remote api", &remote_api)
+
+module Graphiti
+  module SpecHelpers
+    module RSpec
+      def self.included(klass)
+        klass.send(:include, Graphiti::SpecHelpers)
+
+        ::RSpec.configure do |rspec|
+          rspec.include_context "graphiti resource testing", type: :resource
+          rspec.include Graphiti::SpecHelpers::Matchers, type: :resource
+        end
+      end
+
+      def self.schema!(resources = nil, path: nil)
+        ::RSpec.describe "Graphiti Schema" do
+          it "generates a backwards-compatible schema" do
+            check = Graphiti::Schema.check(resources, path: path || Graphiti.config.schema_path)
+            forced = ENV["FORCE_SCHEMA"] == "true"
+            check.write! if check.compatible? || forced
+
+            expect(check.compatible? || forced).to eq(true), check.message
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/graphiti/util/cache_debug.rb b/lib/graphiti/util/cache_debug.rb
index bedbd2e1..f5c77e2a 100644
--- a/lib/graphiti/util/cache_debug.rb
+++ b/lib/graphiti/util/cache_debug.rb
@@ -28,7 +28,7 @@ def current_version
           expires_in: proxy.cache_expires_in,
           etag: proxy.etag,
           miss_count: last_version[:miss_count].to_i + (changed_key? ? 1 : 0),
-          hit_count: last_version[:hit_count].to_i + (!changed_key? && !new_key? ? 1 : 0),
+          hit_count: last_version[:hit_count].to_i + ((!changed_key? && !new_key?) ? 1 : 0),
           request_count: last_version[:request_count].to_i + (last_version.present? ? 1 : 0)
         }
       end
@@ -76,8 +76,8 @@ def added_segments
       end
 
       def changes
-        sub_keys_old = last_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a || []
-        sub_keys_new = current_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a || []
+        sub_keys_old = last_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a
+        sub_keys_new = current_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a
 
         [sub_keys_old, sub_keys_new]
       end
diff --git a/lib/graphiti/util/hash.rb b/lib/graphiti/util/hash.rb
index 38d1a09b..addef5dd 100644
--- a/lib/graphiti/util/hash.rb
+++ b/lib/graphiti/util/hash.rb
@@ -43,7 +43,7 @@ def self.include_removed?(new, old)
       # @return [Hash] the merged hash
       # @api private
       def self.deep_merge!(hash, other)
-        merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
+        merger = proc { |key, v1, v2| (Hash === v1 && Hash === v2) ? v1.merge(v2, &merger) : v2 }
         hash.merge!(other, &merger)
       end
 
@@ -55,8 +55,15 @@ def self.deep_dup(hash)
         else
           {}.tap do |duped|
             hash.each_pair do |key, value|
-              value = deep_dup(value) if value.is_a?(Hash)
-              value = value.dup if value&.respond_to?(:dup) && ![Symbol, Integer].include?(value.class)
+              value = if value.is_a?(Hash)
+                deep_dup(value)
+              elsif value.is_a?(Array)
+                value.map { |element| element.is_a?(Hash) ? deep_dup(element) : element }
+              elsif value&.respond_to?(:dup) && ![Symbol, Integer].include?(value.class)
+                value.dup
+              else
+                value
+              end
               duped[key] = value
             end
           end
diff --git a/lib/graphiti/util/include_params.rb b/lib/graphiti/util/include_params.rb
index 88b56415..836b7ff3 100644
--- a/lib/graphiti/util/include_params.rb
+++ b/lib/graphiti/util/include_params.rb
@@ -7,9 +7,9 @@ class << self
         #
         #   GET /posts?include=comments.author
         #
-        # But our resource had this code:
+        # But the controller had this code:
         #
-        #   sideload_allowlist({ index: [:comments] })
+        #   self.sideload_allowlist = {index: [:comments]}
         #
         # We should drop the 'author' sideload from the request.
         #
diff --git a/lib/graphiti/util/link.rb b/lib/graphiti/util/link.rb
index 9390a2eb..c2f53e0c 100644
--- a/lib/graphiti/util/link.rb
+++ b/lib/graphiti/util/link.rb
@@ -47,7 +47,7 @@ def raw_url
       end
 
       def on_demand_links(url)
-        return url unless Graphiti.config.links_on_demand
+        return url unless @sideload.resource.relationship_links == :on_demand
         return unless url
 
         url << if url.include?("?")
diff --git a/lib/graphiti/util/persistence.rb b/lib/graphiti/util/persistence.rb
index 05e63723..a2d72f52 100644
--- a/lib/graphiti/util/persistence.rb
+++ b/lib/graphiti/util/persistence.rb
@@ -7,8 +7,12 @@ class Graphiti::Util::Persistence
   # @param [Hash] relationships see (Deserializer#relationships)
   # @param [Model] caller_model The persisted parent object in the request graph
   # @param [Symbol] foreign_key Attribute assigned by parent object in graph
-  def initialize(resource, meta, attributes, relationships, caller_model, foreign_key = nil)
+  # @param [Model] assigned_model a model already built by #assign, to be
+  #   assigned onto rather than rebuilt
+  # TODO: make foreign_key a keyword once the satellite gems are rolled in
+  def initialize(resource, meta, attributes, relationships, caller_model, foreign_key = nil, assigned_model: nil)
     @resource = resource
+    @assigned_model = assigned_model
     @meta = meta
     @attributes = attributes
     @relationships = relationships
@@ -24,6 +28,14 @@ def initialize(resource, meta, attributes, relationships, caller_model, foreign_
     end
   end
 
+  def assign
+    attributes = @adapter.persistence_attributes(self, @attributes)
+    assigned = @resource.assign(attributes, metadata, @meta[:method], model_instance: @assigned_model)
+    @resource.decorate_record(assigned)
+
+    assigned
+  end
+
   # Perform the actual save logic.
   #
   # belongs_to must be processed before/separately from has_many -
@@ -46,11 +58,13 @@ def initialize(resource, meta, attributes, relationships, caller_model, foreign_
   def run
     attributes = @adapter.persistence_attributes(self, @attributes)
 
+    payload_attributes = attributes.dup
     parents = @adapter.process_belongs_to(self, attributes)
+    apply_derived_attributes(attributes, payload_attributes)
     persisted = persist_object(@meta[:method], attributes)
     @resource.decorate_record(persisted)
-    assign_temp_id(persisted, @meta[:temp_id])
 
+    assign_temp_id(persisted, @meta[:temp_id])
     associate_parents(persisted, parents)
 
     children = @adapter.process_has_many(self, persisted)
@@ -81,6 +95,19 @@ def iterate(only: [], except: [])
 
   private
 
+  # process_belongs_to persists the parents and writes their primary keys into
+  # +attributes+. A model that was already assigned (see ResourceProxy#assign_attributes)
+  # predates those keys, so apply just them - the payload attributes are already on it,
+  # and re-applying them would clobber any changes made since.
+  def apply_derived_attributes(attributes, payload_attributes)
+    return unless @assigned_model
+
+    derived = attributes.reject { |key, value| payload_attributes[key] == value }
+    return if derived.empty?
+
+    @resource.assign_attributes(@assigned_model, derived, metadata)
+  end
+
   def add_hook(prc, lifecycle_event)
     ::Graphiti::Util::TransactionHooksRecorder.add(prc, lifecycle_event)
   end
@@ -129,12 +156,12 @@ def associate_children(object, children)
 
   def persist_object(method, attributes)
     case method
-      when :destroy
-        call_resource_method(:destroy, attributes[:id], @caller_model)
-      when :update, nil, :disassociate
-        call_resource_method(:update, attributes, @caller_model)
-      else
-        call_resource_method(:create, attributes, @caller_model)
+    when :destroy
+      call_resource_method(:destroy, attributes[:id], @caller_model)
+    when :update, nil, :disassociate
+      call_resource_method(:update, attributes, @caller_model)
+    else
+      call_resource_method(:create, attributes, @caller_model)
     end
   end
 
@@ -163,13 +190,21 @@ def metadata
     }
   end
 
+  # The assigned model rides on the resource instance rather than through
+  # the method signature, so any override of #create/#update - whatever its
+  # arity - passes it through untouched.
   def call_resource_method(method_name, attributes, caller_model)
     method = @resource.method(method_name)
+    call = if method.arity == 1
+      -> { method.call(attributes) }
+    else
+      -> { method.call(attributes, metadata) }
+    end
 
-    if method.arity == 1
-      method.call(attributes)
+    if @assigned_model && [:create, :update].include?(method_name)
+      @resource.with_assigned_model(@assigned_model) { call.call }
     else
-      method.call(attributes, metadata)
+      call.call
     end
   end
 end
diff --git a/lib/graphiti/util/serializer_attributes.rb b/lib/graphiti/util/serializer_attributes.rb
index c192d48c..4b07a571 100644
--- a/lib/graphiti/util/serializer_attributes.rb
+++ b/lib/graphiti/util/serializer_attributes.rb
@@ -30,7 +30,7 @@ def apply
         @serializer.send(:"#{applied_method}=", [@name] | existing)
 
         @serializer.meta do
-          if !!@resource.try(:cursor_paginatable?) && !Graphiti.context[:graphql]
+          if !!@resource.try(:page_cursors?) && !Graphiti.context[:graphql]
             {cursor: cursor}
           end
         end
@@ -121,10 +121,11 @@ def typecast(type)
 
       def default_proc
         name_ref = @name
+        resource_ref = @resource
         typecast_ref = typecast(Graphiti::Types[@attr[:type]][:read])
         ->(_) {
           val = @object.send(name_ref)
-          if Graphiti.config.typecast_reads
+          if resource_ref.typecast_reads
             typecast_ref.call(val)
           else
             val
@@ -133,10 +134,11 @@ def default_proc
       end
 
       def wrap_proc(inner)
+        resource_ref = @resource
         typecast_ref = typecast(Graphiti::Types[@attr[:type]][:read])
         ->(serializer_instance = nil) {
           val = serializer_instance.instance_eval(&inner)
-          if Graphiti.config.typecast_reads
+          if resource_ref.typecast_reads
             typecast_ref.call(val)
           else
             val
diff --git a/lib/graphiti/util/serializer_relationships.rb b/lib/graphiti/util/serializer_relationships.rb
index a75720d3..92b4c5a4 100644
--- a/lib/graphiti/util/serializer_relationships.rb
+++ b/lib/graphiti/util/serializer_relationships.rb
@@ -8,6 +8,8 @@ def initialize(resource_class, sideloads)
       end
 
       def apply
+        return unless @serializer
+
         @sideloads.each_pair do |name, sideload|
           if apply?(sideload)
             SerializerRelationship
@@ -19,7 +21,14 @@ def apply
       private
 
       def apply?(sideload)
-        @serializer.relationship_blocks[sideload.name].nil?
+        return true if @serializer.relationship_blocks[sideload.name].nil?
+
+        # A subclass inherits its parent's relationship blocks, each closed
+        # over the parent's sideload. Redeclaring the relationship has to
+        # replace that block or the override never reaches the payload.
+        # Anything not generated here was written by hand, so leave it.
+        applied = @serializer.relationship_sideloads[sideload.name]
+        !applied.nil? && !applied.equal?(sideload)
       end
     end
 
@@ -32,6 +41,10 @@ def initialize(resource_class, serializer, sideload)
 
       def apply
         sideload = @sideload
+        # Reassign rather than mutate: ancestors share the hash by reference
+        # until a subclass writes to it.
+        @serializer.relationship_sideloads =
+          @serializer.relationship_sideloads.merge(@sideload.name => @sideload)
         @serializer.relationship(@sideload.name, if: -> { sideload.readable? }, &block)
       end
 
@@ -44,8 +57,8 @@ def self.validated_link_cache
       private
 
       def block
-        link_ref = link?
         sideload_ref = @sideload
+        resource_class_ref = @resource_class
         data_proc_ref = data_proc
         self_ref = self
         validate_link! if eagerly_validate_links?
@@ -53,26 +66,71 @@ def block
         proc do
           data { instance_eval(&data_proc_ref) }
 
-          # include relationship links for belongs_to relationships
-          # https://github.com/graphiti-api/graphiti/issues/167
-          linkage always: sideload_ref.always_include_resource_ids?
-
-          if link_ref
-            if @proxy.query.links?
-              self_ref.send(:validate_link!) unless self_ref.send(:eagerly_validate_links?)
+          # An included relationship is already loaded, and a customized
+          # sideload can resolve it to something the foreign key alone would
+          # not predict, so the loaded records win. Only the un-included case
+          # is worth short-circuiting.
+          if sideload_ref.resource_ids_from_foreign_key? &&
+              !::Graphiti::Sideload.assigned?(@proxy.query, @object, sideload_ref.association_name)
+            linkage always: sideload_ref.render_resource_ids? do
+              foreign_key = begin
+                @object.public_send(sideload_ref.foreign_key)
+              rescue NoMethodError => error
+                raise unless defined?(ActiveModel::MissingAttributeError) &&
+                  error.is_a?(ActiveModel::MissingAttributeError)
+
+                raise Errors::UnselectedForeignKey
+                  .new(resource_class_ref, sideload_ref, @object)
+              end
 
-              link(:related) do
-                ::Graphiti::Util::Link.new(sideload_ref, @object).generate
+              unless foreign_key.nil?
+                {
+                  type: sideload_ref.resource.type,
+                  id: foreign_key.to_s
+                }
               end
             end
+          else
+            linkage always: sideload_ref.render_resource_ids?
+          end
+
+          if @proxy.query.render_link?(sideload_ref.link_mode) && self_ref.send(:linkable?)
+            self_ref.send(:validate_link!) unless self_ref.send(:eagerly_validate_links?)
+
+            link(:related) do
+              ::Graphiti::Util::Link.new(sideload_ref, @object).generate
+            end
           end
         end
       end
 
       def data_proc
         sideload_ref = @sideload
+        resource_class_ref = @resource_class
         ->(_) {
-          if (records = @object.public_send(sideload_ref.association_name))
+          begin
+            records = @object.public_send(sideload_ref.association_name)
+          rescue NoMethodError => error
+            # #receiver raises ArgumentError when the error was built by hand
+            # rather than raised by a failed call, and a hand-built one can
+            # still carry a matching #name.
+            receiver = begin
+              error.receiver
+            rescue ArgumentError
+              nil
+            end
+
+            raise unless error.name == sideload_ref.association_name &&
+              receiver.equal?(@object)
+
+            # A private method exists, so "has no such method" would be a lie.
+            raise if @object.respond_to?(sideload_ref.association_name, true)
+
+            raise Errors::MissingRelationshipMethod
+              .new(resource_class_ref, sideload_ref, @object)
+          end
+
+          if records
             if records.respond_to?(:to_ary)
               records.each { |r| sideload_ref.resource.decorate_record(r) }
             else
@@ -85,7 +143,7 @@ def data_proc
       end
 
       def eagerly_validate_links?
-        # TODO: Maybe handle this in graphiti-rails
+        # TODO: Maybe handle this in the Rails integration
         if defined?(::Rails) && (app = ::Rails.application)
           app.config.eager_load
         else
@@ -94,8 +152,8 @@ def eagerly_validate_links?
       end
 
       def validate_link!
-        return unless link?
-        return unless @resource_class.validate_endpoints?
+        return unless @sideload.link? && linkable?
+        return unless @resource_class.validate_links?
         return if @sideload.link_proc
 
         unless Graphiti.config.context_for_endpoint
@@ -114,7 +172,7 @@ def validate_link!
       def validate_link_for_sideload!(sideload)
         return if sideload.resource.remote?
 
-        action = sideload.type == :belongs_to ? :show : :index
+        action = (sideload.type == :belongs_to) ? :show : :index
         cache_key = :"#{@sideload.object_id}-#{action}"
         return if self.class.validated_link_cache.include?(cache_key)
         prc = Graphiti.config.context_for_endpoint
@@ -124,14 +182,16 @@ def validate_link_for_sideload!(sideload)
         self.class.validated_link_cache << cache_key
       end
 
-      def link?
-        return true if @sideload.link_proc
+      # Checked lazily so a sideload with no endpoint only raises when a link is actually wanted.
+      def linkable?
+        return @linkable if defined?(@linkable)
 
-        if @sideload.respond_to?(:children)
-          @sideload.link? &&
-            @sideload.children.values.all? { |c| !c.resource.endpoint.nil? }
+        @linkable = if @sideload.link_proc
+          true
+        elsif @sideload.respond_to?(:children)
+          @sideload.children.values.all? { |c| !c.resource.endpoint.nil? }
         else
-          !!(@sideload.link? && @sideload.resource.endpoint)
+          !@sideload.resource.endpoint.nil?
         end
       end
     end
diff --git a/lib/graphiti/util/simple_errors.rb b/lib/graphiti/util/simple_errors.rb
index 4191a23c..41778997 100644
--- a/lib/graphiti/util/simple_errors.rb
+++ b/lib/graphiti/util/simple_errors.rb
@@ -6,6 +6,21 @@ module Util
     class SimpleErrors
       include Enumerable
 
+      # Overridable under graphiti.errors.messages.
+      DEFAULT_MESSAGES = {
+        missing: "is missing",
+        invalid: "must be an object",
+        invalid_relationship: "is not a valid relationship",
+        unwritable_relationship: "cannot be written",
+        unknown_attribute: "is an unknown attribute",
+        unwritable_attribute: "cannot be written",
+        type_error: "should be type %{type}",
+        attribute_mismatch: "does not match the server endpoint"
+      }.freeze
+
+      # Joins an attribute to its message, like Rails' own errors.format.
+      DEFAULT_FORMAT = "%{attribute} %{message}"
+
       attr_reader :messages, :details
 
       def initialize(validation_target)
@@ -50,11 +65,9 @@ def empty?
       end
       alias_method :blank?, :empty?
 
-      def add(attribute, code, message: nil)
-        message ||= "is #{code.to_s.humanize.downcase}"
-
+      def add(attribute, code, message: nil, **interpolations)
         details[attribute.to_sym] << {error: code}
-        messages[attribute.to_sym] << message
+        messages[attribute.to_sym] << translate(code, message, **interpolations, attribute: attribute)
       end
 
       def added?(attribute, code)
@@ -73,11 +86,19 @@ def full_messages_for(attribute)
 
       def full_message(attribute, message)
         return message if attribute == :base
-        "#{attribute} #{message}"
+
+        translate(:format, DEFAULT_FORMAT, [:graphiti, :errors], attribute: attribute, message: message)
       end
 
       private
 
+      def translate(key, fallback, scope = [:graphiti, :errors, :messages], **interpolations)
+        fallback ||= DEFAULT_MESSAGES.fetch(key) { "is #{key.to_s.humanize.downcase}" }
+        return fallback % interpolations unless defined?(::I18n)
+
+        ::I18n.t(key, scope: scope, default: fallback, **interpolations)
+      end
+
       def apply_default_array(hash)
         hash.default_proc = proc { |h, key| h[key] = [] }
         hash
diff --git a/lib/graphiti/util/transaction_hooks_recorder.rb b/lib/graphiti/util/transaction_hooks_recorder.rb
index 31c95224..26e30728 100644
--- a/lib/graphiti/util/transaction_hooks_recorder.rb
+++ b/lib/graphiti/util/transaction_hooks_recorder.rb
@@ -58,11 +58,11 @@ def run(lifecycle_event)
         private
 
         def _hooks
-          Thread.current[:_graphiti_hooks]
+          Fiber[:_graphiti_hooks]
         end
 
         def reset_hooks
-          Thread.current[:_graphiti_hooks] = {
+          Fiber[:_graphiti_hooks] = {
             after_graph_persist: [],
             before_commit: [],
             after_commit: []
diff --git a/lib/graphiti/version.rb b/lib/graphiti/version.rb
index 44e13c6c..894f71bf 100644
--- a/lib/graphiti/version.rb
+++ b/lib/graphiti/version.rb
@@ -1,3 +1,3 @@
 module Graphiti
-  VERSION = "1.13.4"
+  VERSION = "2.0.0.beta.13"
 end
diff --git a/lib/graphiti_errors.rb b/lib/graphiti_errors.rb
new file mode 100644
index 00000000..2d45791c
--- /dev/null
+++ b/lib/graphiti_errors.rb
@@ -0,0 +1,11 @@
+# Deprecated entry point. graphiti_errors was absorbed into graphiti in 2.0 and
+# graphiti required it for you in 1.x, so an explicit require left in an
+# initializer or spec_helper would otherwise raise LoadError. Only the
+# serializers survived. See graphiti/error_serializers/deprecated_constants.rb.
+# Remove in 3.0.
+require "graphiti"
+
+Graphiti::DEPRECATOR.warn(
+  'require "graphiti_errors" is no longer needed. graphiti_errors is part of graphiti as of 2.0, ' \
+  "and its exception handling is replaced by rescue_registry."
+)
diff --git a/lib/graphiti_spec_helpers.rb b/lib/graphiti_spec_helpers.rb
new file mode 100644
index 00000000..2c988035
--- /dev/null
+++ b/lib/graphiti_spec_helpers.rb
@@ -0,0 +1,11 @@
+# Deprecated entry point. The spec helpers moved under Graphiti::SpecHelpers
+# when the graphiti_spec_helpers gem was absorbed into graphiti in 2.0. This
+# keeps `require "graphiti_spec_helpers"` and the old constant working.
+# Remove in 3.0.
+require "graphiti/spec_helpers"
+
+GraphitiSpecHelpers = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
+  "GraphitiSpecHelpers",
+  "Graphiti::SpecHelpers",
+  Graphiti::DEPRECATOR
+)
diff --git a/lib/graphiti_spec_helpers/rspec.rb b/lib/graphiti_spec_helpers/rspec.rb
new file mode 100644
index 00000000..3ecba3f4
--- /dev/null
+++ b/lib/graphiti_spec_helpers/rspec.rb
@@ -0,0 +1,3 @@
+# Deprecated entry point. See graphiti_spec_helpers.rb. Remove in 3.0.
+require "graphiti/spec_helpers/rspec"
+require "graphiti_spec_helpers"
diff --git a/lib/tasks/graphiti.rake b/lib/tasks/graphiti.rake
new file mode 100644
index 00000000..6c541008
--- /dev/null
+++ b/lib/tasks/graphiti.rake
@@ -0,0 +1,60 @@
+require "graphiti/rails/rake_helpers"
+
+namespace :graphiti do
+  helpers = Graphiti::Rails::RakeHelpers
+
+  desc "Execute request without web server."
+  task :request, [:path, :debug] => [:environment] do |_, args|
+    helpers.setup_rails!
+    Graphiti.logger = Graphiti.stdout_logger
+    Graphiti::Debugger.preserve = true
+    require "pp"
+    path, debug = args[:path], args[:debug]
+    puts "Graphiti Request: #{path}"
+    json = helpers.make_request(path, debug)
+    pp json
+    Graphiti::Debugger.flush if debug
+  end
+
+  namespace :schema do
+    desc "Write the schema file. Refuses backwards-incompatible changes unless FORCE_SCHEMA=true. Takes an optional path, defaulting to Graphiti.config.schema_path."
+    task :generate, [:path] => [:environment] do |_, args|
+      check = Graphiti::Schema.check(path: helpers.schema_path(args[:path]))
+      abort check.message unless check.compatible? || ENV["FORCE_SCHEMA"] == "true"
+
+      puts "Schema written: #{check.write!}"
+    end
+
+    desc "Fail unless the committed schema file exists, is up to date, and is backwards-compatible. Takes an optional path, defaulting to Graphiti.config.schema_path."
+    task :check, [:path] => [:environment] do |_, args|
+      check = Graphiti::Schema.check(path: helpers.schema_path(args[:path]))
+      abort check.message unless check.ok?
+
+      puts check.message
+    end
+  end
+
+  desc "Audit every relationship: what will raise, what loads to render ids, which render no ids, and which checks passed."
+  task audit: [:environment] do
+    helpers.setup_rails!
+    rows = Graphiti::Audit.run
+    puts Graphiti::Audit::Report.new(rows)
+
+    if (advisory = helpers.connection_pool_advisory)
+      puts advisory
+    end
+
+    exit 1 if rows.any?(&:error?)
+  end
+
+  desc "Execute benchmark without web server."
+  task :benchmark, [:path, :requests] => [:environment] do |_, args|
+    helpers.setup_rails!
+    took = Benchmark.ms {
+      args[:requests].to_i.times do
+        helpers.make_request(args[:path])
+      end
+    }
+    puts "Took: #{(took / args[:requests].to_f).round(2)}ms"
+  end
+end
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..8cbcb11b
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,6313 @@
+{
+  "name": "graphiti",
+  "version": "0.0.0-development",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "graphiti",
+      "version": "0.0.0-development",
+      "license": "MIT",
+      "devDependencies": {
+        "@semantic-release/changelog": "^7.0.0",
+        "@semantic-release/exec": "^7.1.0",
+        "@semantic-release/git": "^11.0.1",
+        "semantic-release": "^25.0.8",
+        "semantic-release-rubygem": "^1.2.0"
+      }
+    },
+    "node_modules/@actions/core": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz",
+      "integrity": "sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@actions/exec": "^3.0.0",
+        "@actions/http-client": "^4.0.0"
+      }
+    },
+    "node_modules/@actions/exec": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz",
+      "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@actions/io": "^3.0.2"
+      }
+    },
+    "node_modules/@actions/http-client": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.1.tgz",
+      "integrity": "sha512-+Nvd1ImaOZBSoPbsUtEhv+1z99H12xzncCkz0a3RuehINE81FZSe2QTj3uvAPTcJX/SCzUQHQ0D1GrPMbrPitg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tunnel": "^0.0.6",
+        "undici": "^6.23.0"
+      }
+    },
+    "node_modules/@actions/http-client/node_modules/undici": {
+      "version": "6.28.0",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
+      "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.17"
+      }
+    },
+    "node_modules/@actions/io": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz",
+      "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+      "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.29.7",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.29.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+      "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@colors/colors": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
+      "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=0.1.90"
+      }
+    },
+    "node_modules/@octokit/auth-token": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
+      "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@octokit/core": {
+      "version": "7.0.7",
+      "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.7.tgz",
+      "integrity": "sha512-DcB0M3KFgr9ECI328lhBMVsyFT2DnmNucSBTqEN3exyNKUzkkpUSCHmTRcunF41Eou2TIQKW4seewri8ON9bSA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/auth-token": "^6.0.0",
+        "@octokit/graphql": "^9.0.4",
+        "@octokit/request": "^10.0.13",
+        "@octokit/request-error": "^7.1.1",
+        "@octokit/types": "^17.0.0",
+        "before-after-hook": "^4.0.0",
+        "universal-user-agent": "^7.0.0"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@octokit/endpoint": {
+      "version": "11.0.3",
+      "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz",
+      "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/types": "^16.0.0",
+        "universal-user-agent": "^7.0.2"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": {
+      "version": "27.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
+      "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@octokit/endpoint/node_modules/@octokit/types": {
+      "version": "16.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
+      "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/openapi-types": "^27.0.0"
+      }
+    },
+    "node_modules/@octokit/graphql": {
+      "version": "9.0.4",
+      "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.4.tgz",
+      "integrity": "sha512-5s15CCiY8XXQ+FG+b1YQcl6Z2FA++nwAz/tg2VUrTmnMncP+2nnGUEYANImdnxsA2Fnq+Mbl7hDjUTw7cFAwcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/request": "^10.0.13",
+        "@octokit/types": "^17.0.0",
+        "universal-user-agent": "^7.0.0"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@octokit/openapi-types": {
+      "version": "28.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz",
+      "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@octokit/plugin-paginate-rest": {
+      "version": "14.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
+      "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/types": "^16.0.0"
+      },
+      "engines": {
+        "node": ">= 20"
+      },
+      "peerDependencies": {
+        "@octokit/core": ">=6"
+      }
+    },
+    "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": {
+      "version": "27.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
+      "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": {
+      "version": "16.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
+      "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/openapi-types": "^27.0.0"
+      }
+    },
+    "node_modules/@octokit/plugin-retry": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.1.1.tgz",
+      "integrity": "sha512-VCVvZ/R1+u3WuiBWpNavZ0mY4aaJNAsENrpBP9aLSR2QyOpQgd7DhM5j4AW7z4MQpnJYgwBPf0XqPQoNBRdQwg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/request-error": "^7.1.1",
+        "@octokit/types": "^17.0.0",
+        "bottleneck": "^2.15.3"
+      },
+      "engines": {
+        "node": ">= 20"
+      },
+      "peerDependencies": {
+        "@octokit/core": ">=7"
+      }
+    },
+    "node_modules/@octokit/plugin-throttling": {
+      "version": "11.0.5",
+      "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.5.tgz",
+      "integrity": "sha512-LIdrkrUv+DWbKeg/49rGuFJ3SU0d3hUS+B4MhNZLepBoNUFXms8Ic9edJjrlx+zycqJHjrMRudVpVb/bAXM2Lw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/types": "^17.0.0",
+        "bottleneck": "^2.15.3"
+      },
+      "engines": {
+        "node": ">= 20"
+      },
+      "peerDependencies": {
+        "@octokit/core": "^7.0.0"
+      }
+    },
+    "node_modules/@octokit/request": {
+      "version": "10.0.13",
+      "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.13.tgz",
+      "integrity": "sha512-v2269YxL9Yf+x3d+gRI63FP0vFQEiWgLyBzxe/Y+0yFDg2B/Tzf5dhh9VNfccVAQnfcfwQWyk/y6Bn7rUXXs7A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/endpoint": "^11.0.3",
+        "@octokit/request-error": "^7.1.1",
+        "@octokit/types": "^17.0.0",
+        "content-type": "^2.0.0",
+        "json-with-bigint": "^3.5.3",
+        "universal-user-agent": "^7.0.2"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@octokit/request-error": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.1.tgz",
+      "integrity": "sha512-+eaY7G2VVpSf2pc5Gn1+mph837V/d/TYTJAgWL9Tb0ogGYcpN3IlAVFgjL+Vv93F/sevrxkvsYCedtpLdcFLzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/types": "^17.0.0"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/@octokit/types": {
+      "version": "17.0.0",
+      "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz",
+      "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/openapi-types": "^28.0.0"
+      }
+    },
+    "node_modules/@pnpm/config.env-replace": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
+      "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.22.0"
+      }
+    },
+    "node_modules/@pnpm/network.ca-file": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz",
+      "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "4.2.10"
+      },
+      "engines": {
+        "node": ">=12.22.0"
+      }
+    },
+    "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": {
+      "version": "4.2.10",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/@pnpm/npm-conf": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.3.tgz",
+      "integrity": "sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@pnpm/config.env-replace": "^1.1.0",
+        "@pnpm/network.ca-file": "^1.0.1",
+        "config-chain": "^1.1.11"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@sec-ant/readable-stream": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
+      "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@semantic-release/changelog": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-7.0.0.tgz",
+      "integrity": "sha512-TNPyag5db24o7jWjre7UwKB4EcL8oJxbRhnDQ7hmZRAYqzreAc6PgdxQuU3pppp5xQinYtiumL0iG8SSKvnlzg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@semantic-release/error": "^4.0.0",
+        "aggregate-error": "^5.0.0",
+        "lodash-es": "^4.17.21"
+      },
+      "engines": {
+        "node": "^22.22.2 || >=24.15"
+      },
+      "peerDependencies": {
+        "semantic-release": ">=20.1.0"
+      }
+    },
+    "node_modules/@semantic-release/commit-analyzer": {
+      "version": "13.0.1",
+      "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-13.0.1.tgz",
+      "integrity": "sha512-wdnBPHKkr9HhNhXOhZD5a2LNl91+hs8CC2vsAVYxtZH3y0dV3wKn+uZSN61rdJQZ8EGxzWB3inWocBHV9+u/CQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "conventional-changelog-angular": "^8.0.0",
+        "conventional-changelog-writer": "^8.0.0",
+        "conventional-commits-filter": "^5.0.0",
+        "conventional-commits-parser": "^6.0.0",
+        "debug": "^4.0.0",
+        "import-from-esm": "^2.0.0",
+        "lodash-es": "^4.17.21",
+        "micromatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=20.8.1"
+      },
+      "peerDependencies": {
+        "semantic-release": ">=20.1.0"
+      }
+    },
+    "node_modules/@semantic-release/error": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz",
+      "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@semantic-release/exec": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/@semantic-release/exec/-/exec-7.1.0.tgz",
+      "integrity": "sha512-4ycZ2atgEUutspPZ2hxO6z8JoQt4+y/kkHvfZ1cZxgl9WKJId1xPj+UadwInj+gMn2Gsv+fLnbrZ4s+6tK2TFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@semantic-release/error": "^4.0.0",
+        "aggregate-error": "^3.0.0",
+        "debug": "^4.0.0",
+        "execa": "^9.0.0",
+        "lodash-es": "^4.17.21",
+        "parse-json": "^8.0.0"
+      },
+      "engines": {
+        "node": ">=20.8.1"
+      },
+      "peerDependencies": {
+        "semantic-release": ">=24.1.0"
+      }
+    },
+    "node_modules/@semantic-release/exec/node_modules/aggregate-error": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+      "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "clean-stack": "^2.0.0",
+        "indent-string": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@semantic-release/exec/node_modules/clean-stack": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+      "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/@semantic-release/exec/node_modules/execa": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
+      "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sindresorhus/merge-streams": "^4.0.0",
+        "cross-spawn": "^7.0.6",
+        "figures": "^6.1.0",
+        "get-stream": "^9.0.0",
+        "human-signals": "^8.0.1",
+        "is-plain-obj": "^4.1.0",
+        "is-stream": "^4.0.1",
+        "npm-run-path": "^6.0.0",
+        "pretty-ms": "^9.2.0",
+        "signal-exit": "^4.1.0",
+        "strip-final-newline": "^4.0.0",
+        "yoctocolors": "^2.1.1"
+      },
+      "engines": {
+        "node": "^18.19.0 || >=20.5.0"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/@semantic-release/exec/node_modules/indent-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+      "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/@semantic-release/exec/node_modules/parse-json": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
+      "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.26.2",
+        "index-to-position": "^1.1.0",
+        "type-fest": "^4.39.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@semantic-release/exec/node_modules/type-fest": {
+      "version": "4.41.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+      "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@semantic-release/git": {
+      "version": "11.0.1",
+      "resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-11.0.1.tgz",
+      "integrity": "sha512-Zr8BUYCTZMc8V6wDKN2dpR7nJgewd9I6THL3ydLTnp3OEdTo1/4RBLNYaeRucYMsjMv+BXoCNfXA0NADj1kwhw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@semantic-release/error": "^4.0.0",
+        "aggregate-error": "^5.0.0",
+        "debug": "^4.0.0",
+        "dir-glob": "^3.0.0",
+        "execa": "^10.0.0",
+        "lodash-es": "^4.17.21",
+        "micromatch": "^4.0.0",
+        "p-reduce": "^3.0.0"
+      },
+      "engines": {
+        "node": "^22.22.2 || >=24.15"
+      },
+      "peerDependencies": {
+        "semantic-release": ">=20.1.0"
+      }
+    },
+    "node_modules/@semantic-release/github": {
+      "version": "12.0.9",
+      "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-12.0.9.tgz",
+      "integrity": "sha512-ODIqb0V3QqndipryEEiaBxUQCFjvv7Oese5Dt4omMGa60YRNEW0Sx3K+zri0uac2Y6S9nOlMehciWIzvvRCTGQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@octokit/core": "^7.0.0",
+        "@octokit/plugin-paginate-rest": "^14.0.0",
+        "@octokit/plugin-retry": "^8.0.0",
+        "@octokit/plugin-throttling": "^11.0.0",
+        "@semantic-release/error": "^4.0.0",
+        "aggregate-error": "^5.0.0",
+        "debug": "^4.3.4",
+        "dir-glob": "^3.0.1",
+        "http-proxy-agent": "^9.0.0",
+        "https-proxy-agent": "^9.0.0",
+        "issue-parser": "^7.0.0",
+        "lodash-es": "^4.17.21",
+        "mime": "^4.0.0",
+        "p-filter": "^4.0.0",
+        "tinyglobby": "^0.2.14",
+        "undici": "^7.0.0",
+        "url-join": "^5.0.0"
+      },
+      "engines": {
+        "node": "^22.14.0 || >= 24.10.0"
+      },
+      "peerDependencies": {
+        "semantic-release": ">=24.1.0"
+      }
+    },
+    "node_modules/@semantic-release/npm": {
+      "version": "13.1.5",
+      "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-13.1.5.tgz",
+      "integrity": "sha512-Hq5UxzoatN3LHiq2rTsWS54nCdqJHlsssGERCo8WlvdfFA9LoN0vO+OuKVSjtNapIc/S8C2LBj206wKLHg62mg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@actions/core": "^3.0.0",
+        "@semantic-release/error": "^4.0.0",
+        "aggregate-error": "^5.0.0",
+        "env-ci": "^11.2.0",
+        "execa": "^9.0.0",
+        "fs-extra": "^11.0.0",
+        "lodash-es": "^4.17.21",
+        "nerf-dart": "^1.0.0",
+        "normalize-url": "^9.0.0",
+        "npm": "^11.6.2",
+        "rc": "^1.2.8",
+        "read-pkg": "^10.0.0",
+        "registry-auth-token": "^5.0.0",
+        "semver": "^7.1.2",
+        "tempy": "^3.0.0"
+      },
+      "engines": {
+        "node": "^22.14.0 || >= 24.10.0"
+      },
+      "peerDependencies": {
+        "semantic-release": ">=20.1.0"
+      }
+    },
+    "node_modules/@semantic-release/npm/node_modules/execa": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
+      "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sindresorhus/merge-streams": "^4.0.0",
+        "cross-spawn": "^7.0.6",
+        "figures": "^6.1.0",
+        "get-stream": "^9.0.0",
+        "human-signals": "^8.0.1",
+        "is-plain-obj": "^4.1.0",
+        "is-stream": "^4.0.1",
+        "npm-run-path": "^6.0.0",
+        "pretty-ms": "^9.2.0",
+        "signal-exit": "^4.1.0",
+        "strip-final-newline": "^4.0.0",
+        "yoctocolors": "^2.1.1"
+      },
+      "engines": {
+        "node": "^18.19.0 || >=20.5.0"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/@semantic-release/release-notes-generator": {
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-14.1.1.tgz",
+      "integrity": "sha512-Pbd2e2XRMUD0OxehHpgd5/YghsE76cddkRHSoDvKLK+OCy4Ewxn49rWR631MEUU01lgwF/uyVXvbnVuu6+Z6VA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "conventional-changelog-angular": "^8.0.0",
+        "conventional-changelog-writer": "^8.0.0",
+        "conventional-commits-filter": "^5.0.0",
+        "conventional-commits-parser": "^6.0.0",
+        "debug": "^4.0.0",
+        "import-from-esm": "^2.0.0",
+        "lodash-es": "^4.17.21",
+        "read-package-up": "^11.0.0"
+      },
+      "engines": {
+        "node": ">=20.8.1"
+      },
+      "peerDependencies": {
+        "semantic-release": ">=20.1.0"
+      }
+    },
+    "node_modules/@semantic-release/release-notes-generator/node_modules/hosted-git-info": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
+      "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "lru-cache": "^10.0.1"
+      },
+      "engines": {
+        "node": "^16.14.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@semantic-release/release-notes-generator/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/@semantic-release/release-notes-generator/node_modules/normalize-package-data": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz",
+      "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "hosted-git-info": "^7.0.0",
+        "semver": "^7.3.5",
+        "validate-npm-package-license": "^3.0.4"
+      },
+      "engines": {
+        "node": "^16.14.0 || >=18.0.0"
+      }
+    },
+    "node_modules/@semantic-release/release-notes-generator/node_modules/parse-json": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
+      "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.26.2",
+        "index-to-position": "^1.1.0",
+        "type-fest": "^4.39.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@semantic-release/release-notes-generator/node_modules/read-package-up": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz",
+      "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "find-up-simple": "^1.0.0",
+        "read-pkg": "^9.0.0",
+        "type-fest": "^4.6.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@semantic-release/release-notes-generator/node_modules/read-pkg": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz",
+      "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/normalize-package-data": "^2.4.3",
+        "normalize-package-data": "^6.0.0",
+        "parse-json": "^8.0.0",
+        "type-fest": "^4.6.0",
+        "unicorn-magic": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@semantic-release/release-notes-generator/node_modules/type-fest": {
+      "version": "4.41.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+      "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@semantic-release/release-notes-generator/node_modules/unicorn-magic": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
+      "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@simple-libs/stream-utils": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz",
+      "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://ko-fi.com/dangreen"
+      }
+    },
+    "node_modules/@sindresorhus/is": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+      "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/is?sponsor=1"
+      }
+    },
+    "node_modules/@sindresorhus/merge-streams": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
+      "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@types/normalize-package-data": {
+      "version": "2.4.4",
+      "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+      "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/agent-base": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
+      "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/aggregate-error": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz",
+      "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "clean-stack": "^5.2.0",
+        "indent-string": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ansi-escapes": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
+      "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "environment": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true,
+      "license": "Python-2.0"
+    },
+    "node_modules/argv-formatter": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/argv-formatter/-/argv-formatter-1.0.0.tgz",
+      "integrity": "sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/array-ify": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz",
+      "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/at-least-node": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+      "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/before-after-hook": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
+      "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/bottleneck": {
+      "version": "2.19.5",
+      "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
+      "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.18",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
+      "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "5.6.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+      "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/char-regex": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
+      "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/clean-stack": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.3.0.tgz",
+      "integrity": "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "escape-string-regexp": "5.0.0"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cli-highlight": {
+      "version": "2.1.11",
+      "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz",
+      "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "chalk": "^4.0.0",
+        "highlight.js": "^10.7.1",
+        "mz": "^2.4.0",
+        "parse5": "^5.1.1",
+        "parse5-htmlparser2-tree-adapter": "^6.0.0",
+        "yargs": "^16.0.0"
+      },
+      "bin": {
+        "highlight": "bin/highlight"
+      },
+      "engines": {
+        "node": ">=8.0.0",
+        "npm": ">=5.0.0"
+      }
+    },
+    "node_modules/cli-highlight/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/cli-highlight/node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/cli-highlight/node_modules/cliui": {
+      "version": "7.0.4",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+      "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^7.0.0"
+      }
+    },
+    "node_modules/cli-highlight/node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/cli-highlight/node_modules/yargs": {
+      "version": "16.2.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz",
+      "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^7.0.2",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "require-directory": "^2.1.1",
+        "string-width": "^4.2.0",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^20.2.2"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/cli-highlight/node_modules/yargs-parser": {
+      "version": "20.2.9",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+      "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/cli-table3": {
+      "version": "0.6.5",
+      "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz",
+      "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "string-width": "^4.2.0"
+      },
+      "engines": {
+        "node": "10.* || >= 12.*"
+      },
+      "optionalDependencies": {
+        "@colors/colors": "1.5.0"
+      }
+    },
+    "node_modules/cliui": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+      "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^7.2.0",
+        "strip-ansi": "^7.1.0",
+        "wrap-ansi": "^9.0.0"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/cliui/node_modules/emoji-regex": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+      "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cliui/node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/cliui/node_modules/strip-ansi": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+      "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.2.2"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/compare-func": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz",
+      "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "array-ify": "^1.0.0",
+        "dot-prop": "^5.1.0"
+      }
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/config-chain": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
+      "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ini": "^1.3.4",
+        "proto-list": "~1.2.1"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
+      "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/conventional-changelog-angular": {
+      "version": "8.3.1",
+      "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz",
+      "integrity": "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "compare-func": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/conventional-changelog-writer": {
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-8.4.0.tgz",
+      "integrity": "sha512-HHBFkk1EECxxmCi4CTu091iuDpQv5/OavuCUAuZmrkWpmYfyD816nom1CvtfXJ/uYfAAjavgHvXHX291tSLK8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@simple-libs/stream-utils": "^1.2.0",
+        "conventional-commits-filter": "^5.0.0",
+        "handlebars": "^4.7.7",
+        "meow": "^13.0.0",
+        "semver": "^7.5.2"
+      },
+      "bin": {
+        "conventional-changelog-writer": "dist/cli/index.js"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/conventional-commits-filter": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-5.0.0.tgz",
+      "integrity": "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/conventional-commits-parser": {
+      "version": "6.4.0",
+      "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz",
+      "integrity": "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@simple-libs/stream-utils": "^1.2.0",
+        "meow": "^13.0.0"
+      },
+      "bin": {
+        "conventional-commits-parser": "dist/cli/index.js"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/convert-hrtime": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz",
+      "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cosmiconfig": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz",
+      "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "env-paths": "^2.2.1",
+        "import-fresh": "^3.3.0",
+        "js-yaml": "^4.1.0",
+        "parse-json": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/d-fischer"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.9.5"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cross-spawn/node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/crypto-random-string": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz",
+      "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "type-fest": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/crypto-random-string/node_modules/type-fest": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+      "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deep-extend": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+      "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/dir-glob": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+      "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-type": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dot-prop": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
+      "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-obj": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/duplexer2": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+      "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "readable-stream": "^2.0.2"
+      }
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/emojilib": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz",
+      "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
+    "node_modules/env-ci": {
+      "version": "11.2.0",
+      "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.2.0.tgz",
+      "integrity": "sha512-D5kWfzkmaOQDioPmiviWAVtKmpPT4/iJmMVQxWxMPJTFyTkdc5JQUfc5iXEeWxcOdsYTKSAiA/Age4NUOqKsRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "execa": "^8.0.0",
+        "java-properties": "^1.0.2"
+      },
+      "engines": {
+        "node": "^18.17 || >=20.6.1"
+      }
+    },
+    "node_modules/env-ci/node_modules/execa": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
+      "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cross-spawn": "^7.0.3",
+        "get-stream": "^8.0.1",
+        "human-signals": "^5.0.0",
+        "is-stream": "^3.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^5.1.0",
+        "onetime": "^6.0.0",
+        "signal-exit": "^4.1.0",
+        "strip-final-newline": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=16.17"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/env-ci/node_modules/get-stream": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
+      "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/env-ci/node_modules/human-signals": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
+      "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=16.17.0"
+      }
+    },
+    "node_modules/env-ci/node_modules/is-stream": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/env-ci/node_modules/npm-run-path": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
+      "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^4.0.0"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/env-ci/node_modules/strip-final-newline": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
+      "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/env-paths": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+      "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/environment": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
+      "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/error-ex": {
+      "version": "1.3.4",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+      "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+      "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/execa": {
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-10.0.1.tgz",
+      "integrity": "sha512-ge98qjkRK4IB7tL7Ju/6qmm5LHoH1eEMt5FNZrz3f4UIYhF28lggX20z3FaX1sgc67msLEn0N0BscOs29iuwyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sindresorhus/merge-streams": "^4.0.0",
+        "figures": "^6.1.0",
+        "get-stream": "^9.0.1",
+        "human-signals": "^8.0.1",
+        "is-plain-obj": "^4.1.0",
+        "is-stream": "^4.0.1",
+        "npm-run-path": "^6.0.0",
+        "pretty-ms": "^9.3.0",
+        "signal-exit": "^4.1.0",
+        "strip-final-newline": "^4.0.0",
+        "which-command": "^0.1.0",
+        "yoctocolors": "^2.1.2"
+      },
+      "engines": {
+        "node": ">=22"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/figures": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
+      "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-unicode-supported": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+      "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/find-up-simple": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
+      "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/find-versions": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-6.0.0.tgz",
+      "integrity": "sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "semver-regex": "^4.0.5",
+        "super-regex": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/fs-extra": {
+      "version": "11.4.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz",
+      "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.14"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/function-timeout": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.2.tgz",
+      "integrity": "sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "6.* || 8.* || >= 10.*"
+      }
+    },
+    "node_modules/get-east-asian-width": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
+      "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/get-stream": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
+      "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sec-ant/readable-stream": "^0.4.1",
+        "is-stream": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/git-log-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.1.tgz",
+      "integrity": "sha512-PI+sPDvHXNPl5WNOErAK05s3j0lgwUzMN6o8cyQrDaKfT3qd7TmNJKeXX+SknI5I0QhG5fVPAEwSY4tRGDtYoQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argv-formatter": "~1.0.0",
+        "spawn-error-forwarder": "~1.0.0",
+        "split2": "~1.0.0",
+        "stream-combiner2": "~1.1.1",
+        "through2": "~2.0.0",
+        "traverse": "0.6.8"
+      }
+    },
+    "node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/handlebars": {
+      "version": "4.7.9",
+      "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
+      "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "minimist": "^1.2.5",
+        "neo-async": "^2.6.2",
+        "source-map": "^0.6.1",
+        "wordwrap": "^1.0.0"
+      },
+      "bin": {
+        "handlebars": "bin/handlebars"
+      },
+      "engines": {
+        "node": ">=0.4.7"
+      },
+      "optionalDependencies": {
+        "uglify-js": "^3.1.4"
+      }
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/highlight.js": {
+      "version": "10.7.3",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
+      "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/hook-std": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-4.0.0.tgz",
+      "integrity": "sha512-IHI4bEVOt3vRUDJ+bFA9VUJlo7SzvFARPNLw75pqSmAOP2HmTWfFJtPvLBrDrlgjEYXY9zs7SFdHPQaJShkSCQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/hosted-git-info": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz",
+      "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "lru-cache": "^11.1.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/http-proxy-agent": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz",
+      "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "9.0.0",
+        "debug": "^4.3.4",
+        "proxy-agent-negotiate": "1.1.0"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz",
+      "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "9.0.0",
+        "debug": "^4.3.4",
+        "proxy-agent-negotiate": "1.1.0"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/human-signals": {
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
+      "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/import-fresh/node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/import-from-esm": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-2.0.0.tgz",
+      "integrity": "sha512-YVt14UZCgsX1vZQ3gKjkWVdBdHQ6eu3MPU1TBgL1H5orXe2+jWD006WCPPtOuwlQm10NuzOW5WawiF1Q9veW8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.4",
+        "import-meta-resolve": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18.20"
+      }
+    },
+    "node_modules/import-meta-resolve": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
+      "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
+    "node_modules/indent-string": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
+      "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/index-to-position": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz",
+      "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/ini": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-obj": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
+      "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-plain-obj": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+      "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-stream": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
+      "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-unicode-supported": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+      "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/issue-parser": {
+      "version": "7.0.2",
+      "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.2.tgz",
+      "integrity": "sha512-7atWPjhGEIX3JEtMrOYd8TKzboYlq+5sNbdl9POiLYOI14G5HZiQbZP0Xj5EZdrufQVXfJlpTV0hys0CuxwxZw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lodash.capitalize": "^4.2.1",
+        "lodash.escaperegexp": "^4.1.2",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.isstring": "^4.0.1",
+        "lodash.uniqby": "^4.7.0"
+      },
+      "engines": {
+        "node": "^18.17 || >=20.6.1"
+      }
+    },
+    "node_modules/java-properties": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz",
+      "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/js-yaml": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+      "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/puzrin"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/nodeca"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-parse-better-errors": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+      "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+      "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-with-bigint": {
+      "version": "3.5.10",
+      "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.10.tgz",
+      "integrity": "sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/jsonfile": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+      "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/load-json-file": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+      "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.1.2",
+        "parse-json": "^4.0.0",
+        "pify": "^3.0.0",
+        "strip-bom": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/load-json-file/node_modules/parse-json": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+      "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "error-ex": "^1.3.1",
+        "json-parse-better-errors": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+      "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^2.0.0",
+        "path-exists": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/lodash-es": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+      "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.capitalize": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz",
+      "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.escaperegexp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
+      "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.isstring": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+      "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.uniqby": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz",
+      "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lru-cache": {
+      "version": "11.5.2",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+      "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/make-asynchronous": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz",
+      "integrity": "sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-event": "^6.0.0",
+        "type-fest": "^4.6.0",
+        "web-worker": "^1.5.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/make-asynchronous/node_modules/type-fest": {
+      "version": "4.41.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+      "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/marked": {
+      "version": "15.0.12",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
+      "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/marked-terminal": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz",
+      "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-escapes": "^7.0.0",
+        "ansi-regex": "^6.1.0",
+        "chalk": "^5.4.1",
+        "cli-highlight": "^2.1.11",
+        "cli-table3": "^0.6.5",
+        "node-emoji": "^2.2.0",
+        "supports-hyperlinks": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      },
+      "peerDependencies": {
+        "marked": ">=1 <16"
+      }
+    },
+    "node_modules/meow": {
+      "version": "13.2.0",
+      "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
+      "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz",
+      "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==",
+      "dev": true,
+      "funding": [
+        "https://github.com/sponsors/broofa"
+      ],
+      "license": "MIT",
+      "bin": {
+        "mime": "bin/cli.js"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/mimic-fn": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
+      "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+      "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nerf-dart": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz",
+      "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/node-emoji": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz",
+      "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sindresorhus/is": "^4.6.0",
+        "char-regex": "^1.0.2",
+        "emojilib": "^2.4.0",
+        "skin-tone": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/normalize-package-data": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-8.0.0.tgz",
+      "integrity": "sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "hosted-git-info": "^9.0.0",
+        "semver": "^7.3.5",
+        "validate-npm-package-license": "^3.0.4"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/normalize-url": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-9.0.1.tgz",
+      "integrity": "sha512-ARftfC5HdUNu9jJeL8pHj8debUIHA2b91FizCoMzY4lG6dDX13jdvTK0TBe24IBDRf2HvJSzzwEPvmbkQWHRSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/npm": {
+      "version": "11.19.0",
+      "resolved": "https://registry.npmjs.org/npm/-/npm-11.19.0.tgz",
+      "integrity": "sha512-SDd/hHg3KqHE5Ht2NHWxNYNtqCQ2pXAPLl6OtQhPyED5PHsRfrOtO199MZTIG2cQoQ1ZRI9t28shrD+2cr3AAw==",
+      "bundleDependencies": [
+        "@isaacs/string-locale-compare",
+        "@npmcli/arborist",
+        "@npmcli/config",
+        "@npmcli/fs",
+        "@npmcli/map-workspaces",
+        "@npmcli/metavuln-calculator",
+        "@npmcli/package-json",
+        "@npmcli/promise-spawn",
+        "@npmcli/redact",
+        "@npmcli/run-script",
+        "@sigstore/tuf",
+        "abbrev",
+        "archy",
+        "cacache",
+        "chalk",
+        "ci-info",
+        "fastest-levenshtein",
+        "fs-minipass",
+        "glob",
+        "graceful-fs",
+        "hosted-git-info",
+        "ini",
+        "init-package-json",
+        "is-cidr",
+        "json-parse-even-better-errors",
+        "libnpmaccess",
+        "libnpmdiff",
+        "libnpmexec",
+        "libnpmfund",
+        "libnpmorg",
+        "libnpmpack",
+        "libnpmpublish",
+        "libnpmsearch",
+        "libnpmteam",
+        "libnpmversion",
+        "make-fetch-happen",
+        "minimatch",
+        "minipass",
+        "minipass-pipeline",
+        "ms",
+        "node-gyp",
+        "nopt",
+        "npm-audit-report",
+        "npm-install-checks",
+        "npm-package-arg",
+        "npm-pick-manifest",
+        "npm-profile",
+        "npm-registry-fetch",
+        "npm-user-validate",
+        "p-map",
+        "pacote",
+        "parse-conflict-json",
+        "proc-log",
+        "qrcode-terminal",
+        "read",
+        "semver",
+        "spdx-expression-parse",
+        "ssri",
+        "supports-color",
+        "tar",
+        "text-table",
+        "tiny-relative-date",
+        "treeverse",
+        "validate-npm-package-name",
+        "which"
+      ],
+      "dev": true,
+      "license": "Artistic-2.0",
+      "workspaces": [
+        "docs",
+        "smoke-tests",
+        "mock-globals",
+        "mock-registry",
+        "workspaces/*"
+      ],
+      "dependencies": {
+        "@isaacs/string-locale-compare": "^1.1.0",
+        "@npmcli/arborist": "^9.9.1",
+        "@npmcli/config": "^10.12.0",
+        "@npmcli/fs": "^5.0.0",
+        "@npmcli/map-workspaces": "^5.0.3",
+        "@npmcli/metavuln-calculator": "^9.0.3",
+        "@npmcli/package-json": "^7.0.5",
+        "@npmcli/promise-spawn": "^9.0.1",
+        "@npmcli/redact": "^4.0.0",
+        "@npmcli/run-script": "^10.0.4",
+        "@sigstore/tuf": "^4.0.2",
+        "abbrev": "^4.0.0",
+        "archy": "~1.0.0",
+        "cacache": "^20.0.4",
+        "chalk": "^5.6.2",
+        "ci-info": "^4.4.0",
+        "fastest-levenshtein": "^1.0.16",
+        "fs-minipass": "^3.0.3",
+        "glob": "^13.0.6",
+        "graceful-fs": "^4.2.11",
+        "hosted-git-info": "^9.0.3",
+        "ini": "^6.0.0",
+        "init-package-json": "^8.2.5",
+        "is-cidr": "^6.0.4",
+        "json-parse-even-better-errors": "^5.0.0",
+        "libnpmaccess": "^10.0.3",
+        "libnpmdiff": "^8.1.12",
+        "libnpmexec": "^10.3.2",
+        "libnpmfund": "^7.0.26",
+        "libnpmorg": "^8.0.1",
+        "libnpmpack": "^9.1.12",
+        "libnpmpublish": "^11.2.0",
+        "libnpmsearch": "^9.0.1",
+        "libnpmteam": "^8.0.2",
+        "libnpmversion": "^8.0.4",
+        "make-fetch-happen": "^15.0.6",
+        "minimatch": "^10.2.5",
+        "minipass": "^7.1.3",
+        "minipass-pipeline": "^1.2.4",
+        "ms": "^2.1.2",
+        "node-gyp": "^12.4.0",
+        "nopt": "^9.0.0",
+        "npm-audit-report": "^7.0.0",
+        "npm-install-checks": "^8.0.0",
+        "npm-package-arg": "^13.0.2",
+        "npm-pick-manifest": "^11.0.3",
+        "npm-profile": "^12.0.2",
+        "npm-registry-fetch": "^19.1.1",
+        "npm-user-validate": "^4.0.0",
+        "p-map": "^7.0.4",
+        "pacote": "^21.5.1",
+        "parse-conflict-json": "^5.0.1",
+        "proc-log": "^6.1.0",
+        "qrcode-terminal": "^0.12.0",
+        "read": "^5.0.1",
+        "semver": "^7.8.5",
+        "spdx-expression-parse": "^4.0.0",
+        "ssri": "^13.0.1",
+        "supports-color": "^10.2.2",
+        "tar": "^7.5.19",
+        "text-table": "~0.2.0",
+        "tiny-relative-date": "^2.0.2",
+        "treeverse": "^3.0.0",
+        "validate-npm-package-name": "^7.0.2",
+        "which": "^6.0.1"
+      },
+      "bin": {
+        "npm": "bin/npm-cli.js",
+        "npx": "bin/npx-cli.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm-run-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
+      "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^4.0.0",
+        "unicorn-magic": "^0.3.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/npm/node_modules/@gar/promise-retry": {
+      "version": "1.0.3",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@isaacs/fs-minipass": {
+      "version": "4.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.0.4"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/npm/node_modules/@isaacs/string-locale-compare": {
+      "version": "1.1.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC"
+    },
+    "node_modules/npm/node_modules/@npmcli/agent": {
+      "version": "4.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "http-proxy-agent": "^7.0.0",
+        "https-proxy-agent": "^7.0.1",
+        "lru-cache": "^11.2.1",
+        "socks-proxy-agent": "^8.0.3"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/arborist": {
+      "version": "9.9.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@gar/promise-retry": "^1.0.0",
+        "@isaacs/string-locale-compare": "^1.1.0",
+        "@npmcli/fs": "^5.0.0",
+        "@npmcli/installed-package-contents": "^4.0.0",
+        "@npmcli/map-workspaces": "^5.0.0",
+        "@npmcli/metavuln-calculator": "^9.0.2",
+        "@npmcli/name-from-folder": "^4.0.0",
+        "@npmcli/node-gyp": "^5.0.0",
+        "@npmcli/package-json": "^7.0.0",
+        "@npmcli/query": "^5.0.0",
+        "@npmcli/redact": "^4.0.0",
+        "@npmcli/run-script": "^10.0.0",
+        "bin-links": "^6.0.0",
+        "cacache": "^20.0.1",
+        "common-ancestor-path": "^2.0.0",
+        "hosted-git-info": "^9.0.0",
+        "json-stringify-nice": "^1.1.4",
+        "lru-cache": "^11.2.1",
+        "minimatch": "^10.0.3",
+        "nopt": "^9.0.0",
+        "npm-install-checks": "^8.0.0",
+        "npm-package-arg": "^13.0.0",
+        "npm-pick-manifest": "^11.0.1",
+        "npm-registry-fetch": "^19.0.0",
+        "pacote": "^21.0.2",
+        "parse-conflict-json": "^5.0.1",
+        "proc-log": "^6.0.0",
+        "proggy": "^4.0.0",
+        "promise-all-reject-late": "^1.0.0",
+        "promise-call-limit": "^3.0.1",
+        "semver": "^7.3.7",
+        "ssri": "^13.0.0",
+        "treeverse": "^3.0.0",
+        "walk-up-path": "^4.0.0"
+      },
+      "bin": {
+        "arborist": "bin/index.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/config": {
+      "version": "10.12.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/map-workspaces": "^5.0.0",
+        "@npmcli/package-json": "^7.0.0",
+        "ci-info": "^4.0.0",
+        "ini": "^6.0.0",
+        "nopt": "^9.0.0",
+        "proc-log": "^6.0.0",
+        "semver": "^7.3.5",
+        "walk-up-path": "^4.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/fs": {
+      "version": "5.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/git": {
+      "version": "7.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@gar/promise-retry": "^1.0.0",
+        "@npmcli/promise-spawn": "^9.0.0",
+        "ini": "^6.0.0",
+        "lru-cache": "^11.2.1",
+        "npm-pick-manifest": "^11.0.1",
+        "proc-log": "^6.0.0",
+        "semver": "^7.3.5",
+        "which": "^6.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/installed-package-contents": {
+      "version": "4.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "npm-bundled": "^5.0.0",
+        "npm-normalize-package-bin": "^5.0.0"
+      },
+      "bin": {
+        "installed-package-contents": "bin/index.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/map-workspaces": {
+      "version": "5.0.3",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/name-from-folder": "^4.0.0",
+        "@npmcli/package-json": "^7.0.0",
+        "glob": "^13.0.0",
+        "minimatch": "^10.0.3"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/metavuln-calculator": {
+      "version": "9.0.3",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "cacache": "^20.0.0",
+        "json-parse-even-better-errors": "^5.0.0",
+        "pacote": "^21.0.0",
+        "proc-log": "^6.0.0",
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/name-from-folder": {
+      "version": "4.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/node-gyp": {
+      "version": "5.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/package-json": {
+      "version": "7.0.5",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/git": "^7.0.0",
+        "glob": "^13.0.0",
+        "hosted-git-info": "^9.0.0",
+        "json-parse-even-better-errors": "^5.0.0",
+        "proc-log": "^6.0.0",
+        "semver": "^7.5.3",
+        "spdx-expression-parse": "^4.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/promise-spawn": {
+      "version": "9.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "which": "^6.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/query": {
+      "version": "5.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "postcss-selector-parser": "^7.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/redact": {
+      "version": "4.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@npmcli/run-script": {
+      "version": "10.0.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/node-gyp": "^5.0.0",
+        "@npmcli/package-json": "^7.0.0",
+        "@npmcli/promise-spawn": "^9.0.0",
+        "node-gyp": "^12.1.0",
+        "proc-log": "^6.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@sigstore/bundle": {
+      "version": "4.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@sigstore/protobuf-specs": "^0.5.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@sigstore/core": {
+      "version": "3.2.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@sigstore/protobuf-specs": {
+      "version": "0.5.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.17.0 || >=20.5.0"
+      }
+    },
+    "node_modules/npm/node_modules/@sigstore/sign": {
+      "version": "4.1.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@gar/promise-retry": "^1.0.2",
+        "@sigstore/bundle": "^4.0.0",
+        "@sigstore/core": "^3.2.0",
+        "@sigstore/protobuf-specs": "^0.5.0",
+        "make-fetch-happen": "^15.0.4",
+        "proc-log": "^6.1.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@sigstore/tuf": {
+      "version": "4.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@sigstore/protobuf-specs": "^0.5.0",
+        "tuf-js": "^4.1.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@sigstore/verify": {
+      "version": "3.1.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@sigstore/bundle": "^4.0.0",
+        "@sigstore/core": "^3.2.1",
+        "@sigstore/protobuf-specs": "^0.5.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/@tufjs/canonical-json": {
+      "version": "2.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^16.14.0 || >=18.0.0"
+      }
+    },
+    "node_modules/npm/node_modules/@tufjs/models": {
+      "version": "4.1.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "@tufjs/canonical-json": "2.0.0",
+        "minimatch": "^10.1.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/abbrev": {
+      "version": "4.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/agent-base": {
+      "version": "7.1.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/npm/node_modules/aproba": {
+      "version": "2.1.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC"
+    },
+    "node_modules/npm/node_modules/archy": {
+      "version": "1.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT"
+    },
+    "node_modules/npm/node_modules/balanced-match": {
+      "version": "4.0.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/npm/node_modules/bin-links": {
+      "version": "6.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "cmd-shim": "^8.0.0",
+        "npm-normalize-package-bin": "^5.0.0",
+        "proc-log": "^6.0.0",
+        "read-cmd-shim": "^6.0.0",
+        "write-file-atomic": "^7.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/binary-extensions": {
+      "version": "3.1.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/npm/node_modules/brace-expansion": {
+      "version": "5.0.7",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/npm/node_modules/cacache": {
+      "version": "20.0.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/fs": "^5.0.0",
+        "fs-minipass": "^3.0.0",
+        "glob": "^13.0.0",
+        "lru-cache": "^11.1.0",
+        "minipass": "^7.0.3",
+        "minipass-collect": "^2.0.1",
+        "minipass-flush": "^1.0.5",
+        "minipass-pipeline": "^1.2.4",
+        "p-map": "^7.0.2",
+        "ssri": "^13.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/chalk": {
+      "version": "5.6.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.17.0 || ^14.13 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/npm/node_modules/chownr": {
+      "version": "3.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/npm/node_modules/ci-info": {
+      "version": "4.4.0",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/sibiraj-s"
+        }
+      ],
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/npm/node_modules/cidr-regex": {
+      "version": "5.0.5",
+      "dev": true,
+      "inBundle": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/npm/node_modules/cmd-shim": {
+      "version": "8.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/common-ancestor-path": {
+      "version": "2.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/npm/node_modules/cssesc": {
+      "version": "3.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/npm/node_modules/debug": {
+      "version": "4.4.3",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/npm/node_modules/diff": {
+      "version": "8.0.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/npm/node_modules/env-paths": {
+      "version": "2.2.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/npm/node_modules/exponential-backoff": {
+      "version": "3.1.3",
+      "dev": true,
+      "inBundle": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/npm/node_modules/fastest-levenshtein": {
+      "version": "1.0.16",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4.9.1"
+      }
+    },
+    "node_modules/npm/node_modules/fs-minipass": {
+      "version": "3.0.3",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.0.3"
+      },
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
+    },
+    "node_modules/npm/node_modules/glob": {
+      "version": "13.0.6",
+      "dev": true,
+      "inBundle": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "minimatch": "^10.2.2",
+        "minipass": "^7.1.3",
+        "path-scurry": "^2.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/npm/node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC"
+    },
+    "node_modules/npm/node_modules/hosted-git-info": {
+      "version": "9.0.3",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "lru-cache": "^11.1.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/http-cache-semantics": {
+      "version": "4.2.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/npm/node_modules/http-proxy-agent": {
+      "version": "7.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/npm/node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/npm/node_modules/iconv-lite": {
+      "version": "0.7.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/npm/node_modules/ignore-walk": {
+      "version": "8.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "minimatch": "^10.0.3"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/ini": {
+      "version": "6.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/init-package-json": {
+      "version": "8.2.5",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/package-json": "^7.0.0",
+        "npm-package-arg": "^13.0.0",
+        "promzard": "^3.0.1",
+        "read": "^5.0.1",
+        "semver": "^7.7.2",
+        "validate-npm-package-name": "^7.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/ip-address": {
+      "version": "10.2.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/npm/node_modules/is-cidr": {
+      "version": "6.0.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "cidr-regex": "^5.0.4"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/npm/node_modules/isexe": {
+      "version": "4.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/npm/node_modules/json-parse-even-better-errors": {
+      "version": "5.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/json-stringify-nice": {
+      "version": "1.1.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/npm/node_modules/jsonparse": {
+      "version": "1.3.1",
+      "dev": true,
+      "engines": [
+        "node >= 0.2.0"
+      ],
+      "inBundle": true,
+      "license": "MIT"
+    },
+    "node_modules/npm/node_modules/just-diff": {
+      "version": "6.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT"
+    },
+    "node_modules/npm/node_modules/just-diff-apply": {
+      "version": "5.5.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT"
+    },
+    "node_modules/npm/node_modules/libnpmaccess": {
+      "version": "10.0.3",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "npm-package-arg": "^13.0.0",
+        "npm-registry-fetch": "^19.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/libnpmdiff": {
+      "version": "8.1.12",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/arborist": "^9.9.1",
+        "@npmcli/installed-package-contents": "^4.0.0",
+        "binary-extensions": "^3.0.0",
+        "diff": "^8.0.2",
+        "minimatch": "^10.0.3",
+        "npm-package-arg": "^13.0.0",
+        "pacote": "^21.0.2",
+        "tar": "^7.5.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/libnpmexec": {
+      "version": "10.3.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@gar/promise-retry": "^1.0.0",
+        "@npmcli/arborist": "^9.9.1",
+        "@npmcli/package-json": "^7.0.0",
+        "@npmcli/run-script": "^10.0.0",
+        "ci-info": "^4.0.0",
+        "npm-package-arg": "^13.0.0",
+        "pacote": "^21.0.2",
+        "proc-log": "^6.0.0",
+        "read": "^5.0.1",
+        "semver": "^7.3.7",
+        "signal-exit": "^4.1.0",
+        "walk-up-path": "^4.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/libnpmfund": {
+      "version": "7.0.26",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/arborist": "^9.9.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/libnpmorg": {
+      "version": "8.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "aproba": "^2.0.0",
+        "npm-registry-fetch": "^19.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/libnpmpack": {
+      "version": "9.1.12",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/arborist": "^9.9.1",
+        "@npmcli/run-script": "^10.0.0",
+        "npm-package-arg": "^13.0.0",
+        "pacote": "^21.0.2"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/libnpmpublish": {
+      "version": "11.2.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/package-json": "^7.0.0",
+        "ci-info": "^4.0.0",
+        "npm-package-arg": "^13.0.0",
+        "npm-registry-fetch": "^19.0.0",
+        "proc-log": "^6.0.0",
+        "semver": "^7.3.7",
+        "sigstore": "^4.0.0",
+        "ssri": "^13.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/libnpmsearch": {
+      "version": "9.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "npm-registry-fetch": "^19.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/libnpmteam": {
+      "version": "8.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "aproba": "^2.0.0",
+        "npm-registry-fetch": "^19.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/libnpmversion": {
+      "version": "8.0.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/git": "^7.0.0",
+        "@npmcli/run-script": "^10.0.0",
+        "json-parse-even-better-errors": "^5.0.0",
+        "proc-log": "^6.0.0",
+        "semver": "^7.3.7"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/lru-cache": {
+      "version": "11.5.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/npm/node_modules/make-fetch-happen": {
+      "version": "15.0.6",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@gar/promise-retry": "^1.0.0",
+        "@npmcli/agent": "^4.0.0",
+        "@npmcli/redact": "^4.0.0",
+        "cacache": "^20.0.1",
+        "http-cache-semantics": "^4.1.1",
+        "minipass": "^7.0.2",
+        "minipass-fetch": "^5.0.0",
+        "minipass-flush": "^1.0.5",
+        "minipass-pipeline": "^1.2.4",
+        "negotiator": "^1.0.0",
+        "proc-log": "^6.0.0",
+        "ssri": "^13.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/minimatch": {
+      "version": "10.2.5",
+      "dev": true,
+      "inBundle": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.5"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/npm/node_modules/minipass": {
+      "version": "7.1.3",
+      "dev": true,
+      "inBundle": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/npm/node_modules/minipass-collect": {
+      "version": "2.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.0.3"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/npm/node_modules/minipass-fetch": {
+      "version": "5.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "minipass": "^7.0.3",
+        "minipass-sized": "^2.0.0",
+        "minizlib": "^3.0.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      },
+      "optionalDependencies": {
+        "iconv-lite": "^0.7.2"
+      }
+    },
+    "node_modules/npm/node_modules/minipass-flush": {
+      "version": "1.0.6",
+      "dev": true,
+      "inBundle": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "minipass": "^7.1.3"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/npm/node_modules/minipass-pipeline": {
+      "version": "1.2.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": {
+      "version": "3.3.6",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/npm/node_modules/minipass-pipeline/node_modules/yallist": {
+      "version": "4.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC"
+    },
+    "node_modules/npm/node_modules/minipass-sized": {
+      "version": "2.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/npm/node_modules/minizlib": {
+      "version": "3.1.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/npm/node_modules/ms": {
+      "version": "2.1.3",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT"
+    },
+    "node_modules/npm/node_modules/mute-stream": {
+      "version": "3.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/negotiator": {
+      "version": "1.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/npm/node_modules/node-gyp": {
+      "version": "12.4.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "env-paths": "^2.2.0",
+        "exponential-backoff": "^3.1.1",
+        "graceful-fs": "^4.2.6",
+        "nopt": "^9.0.0",
+        "proc-log": "^6.0.0",
+        "semver": "^7.3.5",
+        "tar": "^7.5.4",
+        "tinyglobby": "^0.2.12",
+        "undici": "^6.25.0",
+        "which": "^6.0.0"
+      },
+      "bin": {
+        "node-gyp": "bin/node-gyp.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/nopt": {
+      "version": "9.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "abbrev": "^4.0.0"
+      },
+      "bin": {
+        "nopt": "bin/nopt.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/npm-audit-report": {
+      "version": "7.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/npm-bundled": {
+      "version": "5.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "npm-normalize-package-bin": "^5.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/npm-install-checks": {
+      "version": "8.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "semver": "^7.1.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/npm-normalize-package-bin": {
+      "version": "5.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/npm-package-arg": {
+      "version": "13.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "hosted-git-info": "^9.0.0",
+        "proc-log": "^6.0.0",
+        "semver": "^7.3.5",
+        "validate-npm-package-name": "^7.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/npm-packlist": {
+      "version": "10.0.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "ignore-walk": "^8.0.0",
+        "proc-log": "^6.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/npm-pick-manifest": {
+      "version": "11.0.3",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "npm-install-checks": "^8.0.0",
+        "npm-normalize-package-bin": "^5.0.0",
+        "npm-package-arg": "^13.0.0",
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/npm-profile": {
+      "version": "12.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "npm-registry-fetch": "^19.0.0",
+        "proc-log": "^6.1.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/npm-registry-fetch": {
+      "version": "19.1.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@npmcli/redact": "^4.0.0",
+        "jsonparse": "^1.3.1",
+        "make-fetch-happen": "^15.0.0",
+        "minipass": "^7.0.2",
+        "minipass-fetch": "^5.0.0",
+        "minizlib": "^3.0.1",
+        "npm-package-arg": "^13.0.0",
+        "proc-log": "^6.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/npm-user-validate": {
+      "version": "4.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/p-map": {
+      "version": "7.0.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/npm/node_modules/pacote": {
+      "version": "21.5.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "@gar/promise-retry": "^1.0.0",
+        "@npmcli/git": "^7.0.0",
+        "@npmcli/installed-package-contents": "^4.0.0",
+        "@npmcli/package-json": "^7.0.0",
+        "@npmcli/promise-spawn": "^9.0.0",
+        "@npmcli/run-script": "^10.0.0",
+        "cacache": "^20.0.0",
+        "fs-minipass": "^3.0.0",
+        "minipass": "^7.0.2",
+        "npm-package-arg": "^13.0.0",
+        "npm-packlist": "^10.0.1",
+        "npm-pick-manifest": "^11.0.1",
+        "npm-registry-fetch": "^19.0.0",
+        "proc-log": "^6.0.0",
+        "sigstore": "^4.0.0",
+        "ssri": "^13.0.0",
+        "tar": "^7.4.3"
+      },
+      "bin": {
+        "pacote": "bin/index.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/parse-conflict-json": {
+      "version": "5.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "json-parse-even-better-errors": "^5.0.0",
+        "just-diff": "^6.0.0",
+        "just-diff-apply": "^5.2.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/path-scurry": {
+      "version": "2.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/npm/node_modules/postcss-selector-parser": {
+      "version": "7.1.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/npm/node_modules/proc-log": {
+      "version": "6.1.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/proggy": {
+      "version": "4.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/promise-all-reject-late": {
+      "version": "1.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/npm/node_modules/promise-call-limit": {
+      "version": "3.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/npm/node_modules/promzard": {
+      "version": "3.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "read": "^5.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/qrcode-terminal": {
+      "version": "0.12.0",
+      "dev": true,
+      "inBundle": true,
+      "bin": {
+        "qrcode-terminal": "bin/qrcode-terminal.js"
+      }
+    },
+    "node_modules/npm/node_modules/read": {
+      "version": "5.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "mute-stream": "^3.0.0"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/read-cmd-shim": {
+      "version": "6.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/npm/node_modules/semver": {
+      "version": "7.8.5",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/npm/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/npm/node_modules/sigstore": {
+      "version": "4.1.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@sigstore/bundle": "^4.0.0",
+        "@sigstore/core": "^3.2.1",
+        "@sigstore/protobuf-specs": "^0.5.0",
+        "@sigstore/sign": "^4.1.1",
+        "@sigstore/tuf": "^4.0.2",
+        "@sigstore/verify": "^3.1.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/smart-buffer": {
+      "version": "4.2.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/npm/node_modules/socks": {
+      "version": "2.8.9",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "ip-address": "^10.1.1",
+        "smart-buffer": "^4.2.0"
+      },
+      "engines": {
+        "node": ">= 10.0.0",
+        "npm": ">= 3.0.0"
+      }
+    },
+    "node_modules/npm/node_modules/socks-proxy-agent": {
+      "version": "8.0.5",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "^4.3.4",
+        "socks": "^2.8.3"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/npm/node_modules/spdx-exceptions": {
+      "version": "2.5.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "CC-BY-3.0"
+    },
+    "node_modules/npm/node_modules/spdx-expression-parse": {
+      "version": "4.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/npm/node_modules/spdx-license-ids": {
+      "version": "3.0.23",
+      "dev": true,
+      "inBundle": true,
+      "license": "CC0-1.0"
+    },
+    "node_modules/npm/node_modules/ssri": {
+      "version": "13.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.0.3"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/supports-color": {
+      "version": "10.2.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/npm/node_modules/tar": {
+      "version": "7.5.19",
+      "dev": true,
+      "inBundle": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/fs-minipass": "^4.0.0",
+        "chownr": "^3.0.0",
+        "minipass": "^7.1.2",
+        "minizlib": "^3.1.0",
+        "yallist": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/npm/node_modules/text-table": {
+      "version": "0.2.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT"
+    },
+    "node_modules/npm/node_modules/tiny-relative-date": {
+      "version": "2.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT"
+    },
+    "node_modules/npm/node_modules/tinyglobby": {
+      "version": "0.2.17",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": {
+      "version": "6.5.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": {
+      "version": "4.0.4",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/npm/node_modules/treeverse": {
+      "version": "3.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
+    },
+    "node_modules/npm/node_modules/tuf-js": {
+      "version": "4.1.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "dependencies": {
+        "@tufjs/models": "4.1.0",
+        "debug": "^4.4.3",
+        "make-fetch-happen": "^15.0.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/undici": {
+      "version": "6.27.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.17"
+      }
+    },
+    "node_modules/npm/node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "MIT"
+    },
+    "node_modules/npm/node_modules/validate-npm-package-name": {
+      "version": "7.0.2",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/walk-up-path": {
+      "version": "4.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/npm/node_modules/which": {
+      "version": "6.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^4.0.0"
+      },
+      "bin": {
+        "node-which": "bin/which.js"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/write-file-atomic": {
+      "version": "7.0.1",
+      "dev": true,
+      "inBundle": true,
+      "license": "ISC",
+      "dependencies": {
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": "^20.17.0 || >=22.9.0"
+      }
+    },
+    "node_modules/npm/node_modules/yallist": {
+      "version": "5.0.0",
+      "dev": true,
+      "inBundle": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/onetime": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
+      "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mimic-fn": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-each-series": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz",
+      "integrity": "sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-event": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz",
+      "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-timeout": "^6.1.2"
+      },
+      "engines": {
+        "node": ">=16.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-filter": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz",
+      "integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-map": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+      "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+      "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/p-map": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz",
+      "integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-reduce": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz",
+      "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-timeout": {
+      "version": "6.1.4",
+      "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz",
+      "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+      "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/parse-json": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+      "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parse-ms": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
+      "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parse5": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
+      "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/parse5-htmlparser2-tree-adapter": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
+      "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parse5": "^6.0.1"
+      }
+    },
+    "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+      "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/path-exists": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+      "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+      "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/path-type": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+      "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+      "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/pkg-conf": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz",
+      "integrity": "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "find-up": "^2.0.0",
+        "load-json-file": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/pretty-ms": {
+      "version": "9.3.0",
+      "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
+      "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parse-ms": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/proto-list": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
+      "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/proxy-agent-negotiate": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz",
+      "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20"
+      },
+      "peerDependencies": {
+        "kerberos": "^2.0.0"
+      },
+      "peerDependenciesMeta": {
+        "kerberos": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pump": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+      "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "node_modules/rc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+      "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+      "dev": true,
+      "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+      "dependencies": {
+        "deep-extend": "^0.6.0",
+        "ini": "~1.3.0",
+        "minimist": "^1.2.0",
+        "strip-json-comments": "~2.0.1"
+      },
+      "bin": {
+        "rc": "cli.js"
+      }
+    },
+    "node_modules/read-package-up": {
+      "version": "12.0.0",
+      "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-12.0.0.tgz",
+      "integrity": "sha512-Q5hMVBYur/eQNWDdbF4/Wqqr9Bjvtrw2kjGxxBbKLbx8bVCL8gcArjTy8zDUuLGQicftpMuU0riQNcAsbtOVsw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "find-up-simple": "^1.0.1",
+        "read-pkg": "^10.0.0",
+        "type-fest": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg": {
+      "version": "10.1.0",
+      "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-10.1.0.tgz",
+      "integrity": "sha512-I8g2lArQiP78ll51UeMZojewtYgIRCKCWqZEgOO8c/uefTI+XDXvCSXu3+YNUaTNvZzobrL5+SqHjBrByRRTdg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/normalize-package-data": "^2.4.4",
+        "normalize-package-data": "^8.0.0",
+        "parse-json": "^8.3.0",
+        "type-fest": "^5.4.4",
+        "unicorn-magic": "^0.4.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg/node_modules/parse-json": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
+      "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.26.2",
+        "index-to-position": "^1.1.0",
+        "type-fest": "^4.39.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg/node_modules/parse-json/node_modules/type-fest": {
+      "version": "4.41.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+      "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/read-pkg/node_modules/unicorn-magic": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz",
+      "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/registry-auth-token": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz",
+      "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@pnpm/npm-conf": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/semantic-release": {
+      "version": "25.0.8",
+      "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-25.0.8.tgz",
+      "integrity": "sha512-w/iZ0bur36rKffXZYmIUmy068eoBY3Ij1DCCddx2JwWEM5Tg+eU9ld/E9qSInVvPASyyR2Ln/XGfQ9OZrMlhtw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@semantic-release/commit-analyzer": "^13.0.1",
+        "@semantic-release/error": "^4.0.0",
+        "@semantic-release/github": "^12.0.0",
+        "@semantic-release/npm": "^13.1.1",
+        "@semantic-release/release-notes-generator": "^14.1.0",
+        "aggregate-error": "^5.0.0",
+        "cosmiconfig": "^9.0.0",
+        "debug": "^4.0.0",
+        "env-ci": "^11.0.0",
+        "execa": "^9.0.0",
+        "figures": "^6.0.0",
+        "find-versions": "^6.0.0",
+        "get-stream": "^6.0.0",
+        "git-log-parser": "^1.2.0",
+        "hook-std": "^4.0.0",
+        "hosted-git-info": "^9.0.0",
+        "import-from-esm": "^2.0.0",
+        "lodash-es": "^4.17.21",
+        "marked": "^15.0.0",
+        "marked-terminal": "^7.3.0",
+        "micromatch": "^4.0.2",
+        "p-each-series": "^3.0.0",
+        "p-reduce": "^3.0.0",
+        "read-package-up": "^12.0.0",
+        "resolve-from": "^5.0.0",
+        "semver": "^7.3.2",
+        "signale": "^1.2.1",
+        "yargs": "^18.0.0"
+      },
+      "bin": {
+        "semantic-release": "bin/semantic-release.js"
+      },
+      "engines": {
+        "node": "^22.14.0 || >= 24.10.0"
+      }
+    },
+    "node_modules/semantic-release-rubygem": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/semantic-release-rubygem/-/semantic-release-rubygem-1.2.0.tgz",
+      "integrity": "sha512-w/McMlrO0cJR7UdTx47tvDJuxRNr0vBf+Xv4nZQ5lOHCbNa07ZhYNRo3EfLxNfnlnCHUCWpYFGdpiofTHldrug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@semantic-release/error": "^2.2.0",
+        "execa": "^4.0.2",
+        "fs-extra": "^9.0.1",
+        "glob": "^7.1.6",
+        "tempy": "^0.5.0"
+      },
+      "engines": {
+        "node": ">=10.18"
+      },
+      "peerDependencies": {
+        "semantic-release": ">=16.0.0 <18.0.0"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/@semantic-release/error": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-2.2.0.tgz",
+      "integrity": "sha512-9Tj/qn+y2j+sjCI3Jd+qseGtHjOAeg7dU2/lVcqIQ9TV3QDaDXDYXcoOHU+7o2Hwh8L8ymL4gfuO7KxDs3q2zg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/semantic-release-rubygem/node_modules/crypto-random-string": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
+      "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/execa": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz",
+      "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cross-spawn": "^7.0.0",
+        "get-stream": "^5.0.0",
+        "human-signals": "^1.1.1",
+        "is-stream": "^2.0.0",
+        "merge-stream": "^2.0.0",
+        "npm-run-path": "^4.0.0",
+        "onetime": "^5.1.0",
+        "signal-exit": "^3.0.2",
+        "strip-final-newline": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/fs-extra": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+      "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "at-least-node": "^1.0.0",
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/get-stream": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+      "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pump": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/human-signals": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz",
+      "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8.12.0"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/is-stream": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/mimic-fn": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
+      "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/npm-run-path": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
+      "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/onetime": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
+      "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mimic-fn": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/semantic-release-rubygem/node_modules/strip-final-newline": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
+      "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/temp-dir": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
+      "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/tempy": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.5.0.tgz",
+      "integrity": "sha512-VEY96x7gbIRfsxqsafy2l5yVxxp3PhwAGoWMyC2D2Zt5DmEv+2tGiPOrquNRpf21hhGnKLVEsuqleqiZmKG/qw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-stream": "^2.0.0",
+        "temp-dir": "^2.0.0",
+        "type-fest": "^0.12.0",
+        "unique-string": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/type-fest": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.12.0.tgz",
+      "integrity": "sha512-53RyidyjvkGpnWPMF9bQgFtWp+Sl8O2Rp13VavmJgfAP9WWG6q6TkrKU8iyJdnwnfgHI6k2hTlgqH4aSdjoTbg==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/semantic-release-rubygem/node_modules/unique-string": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
+      "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "crypto-random-string": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/semantic-release/node_modules/execa": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz",
+      "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sindresorhus/merge-streams": "^4.0.0",
+        "cross-spawn": "^7.0.6",
+        "figures": "^6.1.0",
+        "get-stream": "^9.0.0",
+        "human-signals": "^8.0.1",
+        "is-plain-obj": "^4.1.0",
+        "is-stream": "^4.0.1",
+        "npm-run-path": "^6.0.0",
+        "pretty-ms": "^9.2.0",
+        "signal-exit": "^4.1.0",
+        "strip-final-newline": "^4.0.0",
+        "yoctocolors": "^2.1.1"
+      },
+      "engines": {
+        "node": "^18.19.0 || >=20.5.0"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/execa?sponsor=1"
+      }
+    },
+    "node_modules/semantic-release/node_modules/execa/node_modules/get-stream": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
+      "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@sec-ant/readable-stream": "^0.4.1",
+        "is-stream": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/semantic-release/node_modules/get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+      "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/semver-regex": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz",
+      "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/signale": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz",
+      "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^2.3.2",
+        "figures": "^2.0.0",
+        "pkg-conf": "^2.1.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/signale/node_modules/ansi-styles": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+      "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/signale/node_modules/chalk": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^3.2.1",
+        "escape-string-regexp": "^1.0.5",
+        "supports-color": "^5.3.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/signale/node_modules/color-convert": {
+      "version": "1.9.3",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "1.1.3"
+      }
+    },
+    "node_modules/signale/node_modules/color-name": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/signale/node_modules/escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/signale/node_modules/figures": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+      "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "escape-string-regexp": "^1.0.5"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/signale/node_modules/has-flag": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/signale/node_modules/supports-color": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+      "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/skin-tone": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz",
+      "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "unicode-emoji-modifier-base": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/spawn-error-forwarder": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz",
+      "integrity": "sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/spdx-correct": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+      "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "spdx-expression-parse": "^3.0.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-exceptions": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+      "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+      "dev": true,
+      "license": "CC-BY-3.0"
+    },
+    "node_modules/spdx-expression-parse": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+      "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "spdx-exceptions": "^2.1.0",
+        "spdx-license-ids": "^3.0.0"
+      }
+    },
+    "node_modules/spdx-license-ids": {
+      "version": "3.0.23",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz",
+      "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==",
+      "dev": true,
+      "license": "CC0-1.0"
+    },
+    "node_modules/split2": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/split2/-/split2-1.0.0.tgz",
+      "integrity": "sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "through2": "~2.0.0"
+      }
+    },
+    "node_modules/stream-combiner2": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz",
+      "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "duplexer2": "~0.1.0",
+        "readable-stream": "^2.0.2"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/strip-final-newline": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
+      "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/super-regex": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.1.0.tgz",
+      "integrity": "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-timeout": "^1.0.1",
+        "make-asynchronous": "^1.0.1",
+        "time-span": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-hyperlinks": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz",
+      "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0",
+        "supports-color": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1"
+      }
+    },
+    "node_modules/tagged-tag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
+      "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/temp-dir": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz",
+      "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.16"
+      }
+    },
+    "node_modules/tempy": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.2.0.tgz",
+      "integrity": "sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-stream": "^3.0.0",
+        "temp-dir": "^3.0.0",
+        "type-fest": "^2.12.2",
+        "unique-string": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/tempy/node_modules/is-stream": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+      "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/tempy/node_modules/type-fest": {
+      "version": "2.19.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+      "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=12.20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/through2": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+      "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readable-stream": "~2.3.6",
+        "xtend": "~4.0.1"
+      }
+    },
+    "node_modules/time-span": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz",
+      "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "convert-hrtime": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.17",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+      "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinyglobby/node_modules/picomatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+      "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/traverse": {
+      "version": "0.6.8",
+      "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz",
+      "integrity": "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tunnel": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
+      "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
+      }
+    },
+    "node_modules/type-fest": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz",
+      "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "dependencies": {
+        "tagged-tag": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/uglify-js": {
+      "version": "3.19.3",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+      "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "optional": true,
+      "bin": {
+        "uglifyjs": "bin/uglifyjs"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/undici": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
+      "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=20.18.1"
+      }
+    },
+    "node_modules/unicode-emoji-modifier-base": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz",
+      "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/unicorn-magic": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+      "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/unique-string": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz",
+      "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "crypto-random-string": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/universal-user-agent": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
+      "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/universalify": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+      "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/url-join": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz",
+      "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/validate-npm-package-license": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+      "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "spdx-correct": "^3.0.0",
+        "spdx-expression-parse": "^3.0.0"
+      }
+    },
+    "node_modules/web-worker": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
+      "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/which-command": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/which-command/-/which-command-0.1.0.tgz",
+      "integrity": "sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "which-command": "cli.js"
+      },
+      "engines": {
+        "node": ">=22"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/which-command?sponsor=1"
+      }
+    },
+    "node_modules/wordwrap": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+      "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi": {
+      "version": "9.0.2",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+      "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.2.1",
+        "string-width": "^7.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/emoji-regex": {
+      "version": "10.6.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+      "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi/node_modules/string-width": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+      "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^10.3.0",
+        "get-east-asian-width": "^1.0.0",
+        "strip-ansi": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/wrap-ansi/node_modules/strip-ansi": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+      "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.2.2"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
+    "node_modules/y18n": {
+      "version": "5.0.8",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+      "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/yargs": {
+      "version": "18.1.0",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz",
+      "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^9.0.1",
+        "escalade": "^3.1.1",
+        "get-caller-file": "^2.0.5",
+        "string-width": "^8.2.1",
+        "y18n": "^5.0.5",
+        "yargs-parser": "^22.0.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=23"
+      }
+    },
+    "node_modules/yargs-parser": {
+      "version": "22.0.0",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
+      "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=23"
+      }
+    },
+    "node_modules/yargs/node_modules/string-width": {
+      "version": "8.2.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
+      "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "get-east-asian-width": "^1.5.0",
+        "strip-ansi": "^7.1.2"
+      },
+      "engines": {
+        "node": ">=20"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yargs/node_modules/strip-ansi": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+      "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.2.2"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/yoctocolors": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz",
+      "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
index 60015443..3f69ecf1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
 {
   "name": "graphiti",
-  "version": "1.4.0",
+  "private": true,
+  "version": "0.0.0-development",
   "repository": {
     "type": "git",
     "url": "git+https://github.com/graphiti-api/graphiti.git"
@@ -15,12 +16,14 @@
   },
   "devDependencies": {
     "semantic-release-rubygem": "^1.2.0",
-    "semantic-release": "^19.0.3",
-    "@semantic-release/changelog": "^6.0.1",
-    "@semantic-release/git": "^10.0.1" 
+    "semantic-release": "^25.0.8",
+    "@semantic-release/changelog": "^7.0.0",
+    "@semantic-release/git": "^11.0.1",
+    "@semantic-release/exec": "^7.1.0"
   },
   "release": {
     "branches": [
+      "1.x",
       "main",
       {
         "name": "beta",
@@ -130,13 +133,25 @@
         }
       ],
       "semantic-release-rubygem",
-      "@semantic-release/github",
+      [
+        "@semantic-release/github",
+        {
+          "successCommentCondition": "<% return !branch.prerelease; %>"
+        }
+      ],
+      [
+        "@semantic-release/exec",
+        {
+          "prepareCmd": "bundle exec ruby spec/performance/measure_releases.rb --promote v${nextRelease.version}"
+        }
+      ],
       [
         "@semantic-release/git",
         {
           "assets": [
             "CHANGELOG.md",
-            "lib/graphiti/version.rb"
+            "lib/graphiti/version.rb",
+            "spec/performance/performance_history.tsv"
           ],
           "message": "${nextRelease.version} CHANGELOG [skip ci]\n\n${nextRelease.notes}"
         }
diff --git a/spec/audit_spec.rb b/spec/audit_spec.rb
new file mode 100644
index 00000000..f616f48d
--- /dev/null
+++ b/spec/audit_spec.rb
@@ -0,0 +1,588 @@
+require "spec_helper"
+
+RSpec.describe Graphiti::Audit do
+  def audit(resource_class)
+    described_class.findings([resource_class])
+  end
+
+  def finding(resource_class, check)
+    audit(resource_class).find { |f| f.check == check }
+  end
+
+  describe "missing association method" do
+    let(:resource) do
+      Class.new(PORO::TeamResource) do
+        def self.name
+          "PORO::TeamResource"
+        end
+
+        has_many :employees
+      end
+    end
+
+    it "reports the resource, the relationship and the model" do
+      row = described_class.run([resource]).find { |r| r.relationship == :employees }
+
+      expect(row.resource).to eq("PORO::TeamResource")
+      expect(row.type).to eq(:has_many)
+      expect(row.resource_ids_source).to eq(:none)
+      expect(row.severity).to eq(:error)
+      expect(row.findings.first.message).to eq("PORO::Team has no #employees method")
+    end
+
+    it "accepts a private method, since rendering only needs it to exist" do
+      PORO::Team.class_eval { private def employees = [] }
+
+      expect(finding(resource, :missing_association_method)).to be_nil
+    ensure
+      PORO::Team.send(:remove_method, :employees)
+    end
+
+    it "follows `as:` rather than the relationship name" do
+      klass = Class.new(PORO::TeamResource) do
+        def self.name
+          "PORO::TeamResource"
+        end
+
+        has_many :staff, as: :employees, resource: PORO::EmployeeResource
+      end
+
+      expect(finding(klass, :missing_association_method).message)
+        .to eq("PORO::Team has no #employees method")
+    end
+  end
+
+  describe "missing guard method" do
+    it "reports a readable guard defined on neither resource" do
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        belongs_to :employee, readable: :nobody_defines_this?
+      end
+
+      found = finding(klass, :missing_guard_method)
+
+      expect(found.severity).to eq(:error)
+      expect(found.message).to match(/#nobody_defines_this\? is defined on neither/)
+    end
+
+    it "accepts a guard defined on the declaring resource" do
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        belongs_to :employee, readable: :admin?
+
+        def admin?
+          true
+        end
+      end
+
+      expect(finding(klass, :missing_guard_method)).to be_nil
+    end
+
+    it "ignores a proc guard, which has no name to look up" do
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        belongs_to :employee, readable: -> { true }
+      end
+
+      expect(finding(klass, :missing_guard_method)).to be_nil
+    end
+  end
+
+  describe "rendering ids by loading" do
+    def row_for(resource_class)
+      described_class.run([resource_class]).find { |row| row.relationship == :employee }
+    end
+
+    it "reports :load without a finding, since opting in is a choice, not a defect" do
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        belongs_to :employee, resource_ids: true do
+          scope { |ids| {type: :employees, conditions: {id: ids}} }
+        end
+      end
+
+      row = row_for(klass)
+
+      expect(row.resource_ids_source).to eq(:load)
+      expect(row.findings).to eq([])
+    end
+
+    describe "whether base_scope preloads the loaded association" do
+      let(:scope_class) { Struct.new(:includes_values, :preload_values, :eager_load_values) }
+
+      def loading_resource(base_scope)
+        Class.new(PORO::PositionResource) do
+          def self.name
+            "PORO::PositionResource"
+          end
+
+          define_method(:base_scope) { base_scope }
+
+          belongs_to :employee, resource_ids: true do
+            scope { |ids| {type: :employees, conditions: {id: ids}} }
+          end
+        end
+      end
+
+      it "sees a preload, including a nested one" do
+        row = row_for(loading_resource(scope_class.new([{employee: :department}], [], [])))
+
+        expect(row.preloaded).to eq(true)
+      end
+
+      it "sees a missing preload" do
+        row = row_for(loading_resource(scope_class.new([:other], [], [])))
+
+        expect(row.preloaded).to eq(false)
+      end
+
+      it "answers nil for a scope with no preload lists to read" do
+        row = row_for(loading_resource({type: :positions}))
+
+        expect(row.preloaded).to be_nil
+      end
+    end
+
+    it "reports :load under an API-wide :always" do
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        self.belongs_to_resource_ids_by_default = :always
+
+        belongs_to :employee, primary_key: :first_name
+      end
+
+      expect(row_for(klass).resource_ids_source).to eq(:load)
+    end
+
+    it "reports :key for a relationship reading ids off the foreign key" do
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        belongs_to :employee
+      end
+
+      expect(row_for(klass).resource_ids_source).to eq(:key)
+    end
+
+    it "notes nothing under :never, which asked for exactly this" do
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        self.belongs_to_resource_ids_by_default = :never
+
+        belongs_to :employee do
+          scope { |ids| {type: :employees, conditions: {id: ids}} }
+        end
+      end
+
+      row = row_for(klass)
+
+      expect(row.resource_ids_source).to eq(:none)
+      expect(row.would_start_loading?).to eq(false)
+    end
+
+    it "reports :none for a relationship rendering no ids at all" do
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        belongs_to :employee do
+          scope { |ids| {type: :employees, conditions: {id: ids}} }
+        end
+      end
+
+      expect(row_for(klass).resource_ids_source).to eq(:none)
+    end
+  end
+
+  describe "a relationship that raises while being inspected" do
+    let(:resource) do
+      Class.new(PORO::PositionResource) do
+        def self.name
+          "NotNamespacedResource"
+        end
+
+        belongs_to :employee
+      end
+    end
+
+    it "reports it rather than taking the whole report down" do
+      rows = described_class.run([resource])
+      row = rows.find { |r| r.relationship == :employee }
+
+      expect(row.severity).to eq(:error)
+      expect(row.findings.first.check).to eq(:broken_relationship)
+      expect(row.findings.first.message)
+        .to match(/Could not find resource class for sideload 'employee'/)
+    end
+
+    it "still reports the resource's other relationships" do
+      expect(described_class.run([resource]).map(&:relationship)).to include(:department)
+    end
+  end
+
+  describe "a clean resource" do
+    it "reports nothing" do
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        belongs_to :employee
+      end
+
+      expect(audit(klass)).to eq([])
+    end
+  end
+end
+
+RSpec.describe Graphiti::Audit::Report do
+  let(:rows) do
+    klass = Class.new(PORO::PositionResource) do
+      def self.name
+        "PORO::PositionResource"
+      end
+
+      belongs_to :employee
+    end
+
+    Graphiti::Audit.run([klass])
+  end
+
+  describe "a relationship loading to render ids" do
+    def output_for(base_scope)
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        define_method(:base_scope) { base_scope }
+
+        belongs_to :employee, resource_ids: true do
+          scope { |ids| {type: :employees, conditions: {id: ids}} }
+        end
+      end
+
+      described_class.new(Graphiti::Audit.run([klass]), color: false).to_s
+    end
+
+    let(:scope_class) { Struct.new(:includes_values, :preload_values, :eager_load_values) }
+
+    it "warns when base_scope does not preload the association" do
+      output = output_for(scope_class.new([], [], []))
+
+      expect(output).to include("WARNING rendering resource ids by loading an association the base_scope does not preload")
+      expect(output).to include("fix: preload the association in base_scope, or drop `resource_ids`")
+      expect(output).to include("1 loading ids without preloading")
+    end
+
+    it "says nothing when base_scope preloads it" do
+      output = output_for(scope_class.new([:employee], [], []))
+
+      expect(output).to_not include("WARNING")
+      expect(output).to include("✓ all id-rendering loads preloaded")
+      expect(output).to_not include("(none)")
+    end
+
+    it "drops the check when nothing loads to render ids" do
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        self.belongs_to_resource_ids_by_default = :never
+
+        belongs_to :employee
+      end
+
+      output = described_class.new(Graphiti::Audit.run([klass]), color: false).to_s
+
+      expect(output).to_not include("id-rendering loads")
+    end
+
+    it "says nothing when the scope cannot be inspected" do
+      expect(output_for({type: :positions})).to_not include("WARNING")
+    end
+  end
+
+  it "lists every check, with a count for the failed ones" do
+    output = described_class.new(rows, color: false).to_s
+
+    expect(output).to include("checks")
+    expect(output).to include("✓ all association methods defined")
+    expect(output).to include("✓ all sideload filters declared")
+  end
+
+  it "marks a failed check with a red x and the count" do
+    klass = Class.new(PORO::TeamResource) do
+      def self.name
+        "PORO::TeamResource"
+      end
+
+      has_many :employees
+    end
+
+    output = described_class.new(Graphiti::Audit.run([klass]), color: false).to_s
+
+    expect(output).to include("✗ 1 association method missing")
+    expect(output).to include("✗ 1 sideload filter missing")
+  end
+
+  it "shows only the passed checks when there is nothing wrong" do
+    output = described_class.new(rows, color: false).to_s
+
+    expect(output).to_not include("ERROR")
+    expect(output).to_not include("FYI")
+    expect(output.scan(/^  ✓ /).size).to eq(Graphiti::Audit::Report::CHECKLIST.size - 1)
+    expect(output).to_not include("✗")
+  end
+
+  it "repeats back the options that differ from the defaults" do
+    klass = Class.new(PORO::PositionResource) do
+      def self.name
+        "PORO::PositionResource"
+      end
+
+      belongs_to :employee, primary_key: :first_name, single: true, link: false
+    end
+
+    output = described_class.new(Graphiti::Audit.run([klass]), color: false).to_s
+
+    expect(output).to include("belongs_to :employee, primary_key: :first_name, single: true, link: false")
+  end
+
+  it "summarises" do
+    output = described_class.new(rows, color: false).to_s
+
+    expect(output).to match(/1 resource, \d+ relationships, 0 errors/)
+  end
+
+  it "says so when there is nothing to report" do
+    expect(described_class.new([]).to_s).to eq("graphiti: no relationships found.\n")
+  end
+
+  describe "the :always column" do
+    let(:rows) do
+      klass = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        belongs_to :employee do
+          scope { |ids| {type: :employees, conditions: {id: ids}} }
+        end
+      end
+
+      Graphiti::Audit.run([klass])
+    end
+
+    let(:row) { rows.find { |r| r.relationship == :employee } }
+
+    it "shows what a relationship would cost without changing what it does now" do
+      expect(row.resource_ids_source).to eq(:none)
+      expect(row.resource_ids_source_if_always).to eq(:load)
+      expect(row.would_start_loading?).to eq(true)
+    end
+
+    it "leaves the resource's own setting untouched" do
+      rows
+
+      expect(PORO::PositionResource.belongs_to_resource_ids_by_default).to eq(:foreign_key)
+    end
+
+    it "shows which relationships would start loading, and why" do
+      output = described_class.new(rows, color: false).to_s
+
+      expect(output).to include("1 belongs_to relationship renders no resource ids")
+      expect(output).to include("To render ids on every response, opt in:")
+      expect(output).to match(/^    resource_ids: true\s+on one relationship$/)
+      expect(output).to match(/^  PORO::PositionResource\n    belongs_to :employee/)
+      expect(output).to include("Nothing to fix.")
+      expect(output).to match(/1 without resource ids/)
+    end
+  end
+end
+
+RSpec.describe "Graphiti::Audit::Report colour" do
+  let(:rows) do
+    klass = Class.new(PORO::TeamResource) do
+      def self.name
+        "PORO::TeamResource"
+      end
+
+      has_many :employees
+    end
+
+    Graphiti::Audit.run([klass])
+  end
+
+  it "is off when the output is not a terminal" do
+    expect(Graphiti::Audit::Report.new(rows, color: false).to_s).to_not include("\e[")
+  end
+
+  it "paints an error row red when it is" do
+    output = Graphiti::Audit::Report.new(rows, color: true).to_s
+
+    expect(output).to include("\e[31;1mERROR")
+  end
+
+  it "keeps columns aligned regardless of colour" do
+    plain = Graphiti::Audit::Report.new(rows, color: false).to_s
+    painted = Graphiti::Audit::Report.new(rows, color: true).to_s.gsub(/\e\[[\d;]+m/, "")
+
+    expect(painted).to eq(plain)
+  end
+end
+
+RSpec.describe "Graphiti::Audit::Report layout" do
+  let(:rows) do
+    visa = PORO::VisaResource
+    klass = Class.new(PORO::EmployeeResource) do
+      def self.name
+        "PORO::EmployeeResource"
+      end
+
+      has_many :positions
+      belongs_to :classification
+      polymorphic_belongs_to :credit_card do
+        group_by(:credit_card_type) do
+          on(:Visa).belongs_to :visa, resource: visa
+        end
+      end
+    end
+
+    Graphiti::Audit.run([klass])
+  end
+
+  it "groups relationships with the same issue under one heading, stating the fix once" do
+    team = Class.new(PORO::TeamResource) do
+      def self.name
+        "PORO::TeamResource"
+      end
+
+      has_many :employees
+    end
+    position = Class.new(PORO::PositionResource) do
+      def self.name
+        "PORO::PositionResource"
+      end
+
+      has_many :teams, resource: PORO::TeamResource
+    end
+
+    output = Graphiti::Audit::Report.new(Graphiti::Audit.run([team, position]), color: false).to_s
+
+    expect(output.scan("ERROR will raise when the relationship is included: the model has no association method").size).to eq(1)
+    expect(output.scan(/^  fix: define it/).size).to eq(1)
+    expect(output).to match(/PORO::PositionResource\s+has_many :teams\s+PORO::Position has no #teams method/)
+    expect(output).to match(/PORO::TeamResource\s+has_many :employees\s+PORO::Team has no #employees method/)
+  end
+
+  it "names the children of a polymorphic_belongs_to rather than nothing" do
+    row = rows.find { |r| r.relationship == :credit_card }
+
+    expect(row.target).to eq("VisaResource")
+  end
+end
+
+RSpec.describe "Graphiti::Audit new checks" do
+  def finding(resource_class, check, relationship: :positions)
+    row = Graphiti::Audit.run([resource_class]).find { |r| r.relationship == relationship }
+    row.findings.find { |f| f.check == check }
+  end
+
+  describe "missing sideload filter" do
+    let(:unfilterable) do
+      Class.new(Graphiti::Resource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+        self.model = PORO::Position
+        self.adapter = Graphiti::Adapters::Null
+        self.type = :positions
+        attribute :title, :string
+      end
+    end
+
+    it "reports a has_many whose resource cannot be filtered by the foreign key" do
+      target = unfilterable
+      klass = Class.new(PORO::EmployeeResource) do
+        def self.name
+          "PORO::EmployeeResource"
+        end
+
+        has_many :positions, resource: target
+      end
+
+      found = finding(klass, :missing_sideload_filter)
+
+      expect(found.severity).to eq(:error)
+      expect(found.message).to match(/is missing `filter :employee_id`/)
+      expect(found.remedy).to eq("declare the filter on the related resource")
+    end
+
+    it "says nothing when a params block builds the query, since it can replace the filter" do
+      target = unfilterable
+      klass = Class.new(PORO::EmployeeResource) do
+        def self.name
+          "PORO::EmployeeResource"
+        end
+
+        has_many :positions, resource: target do
+          params do |hash|
+            hash[:filter] = {id: 1}
+          end
+        end
+      end
+
+      expect(finding(klass, :missing_sideload_filter)).to be_nil
+    end
+
+    it "says nothing when a scope block builds the query" do
+      target = unfilterable
+      klass = Class.new(PORO::EmployeeResource) do
+        def self.name
+          "PORO::EmployeeResource"
+        end
+
+        has_many :positions, resource: target do
+          scope { |ids| {type: :positions, conditions: {employee_id: ids}} }
+        end
+      end
+
+      expect(finding(klass, :missing_sideload_filter)).to be_nil
+    end
+
+    it "says nothing when the filter is there" do
+      klass = Class.new(PORO::EmployeeResource) do
+        def self.name
+          "PORO::EmployeeResource"
+        end
+
+        has_many :positions
+      end
+
+      expect(finding(klass, :missing_sideload_filter)).to be_nil
+    end
+  end
+end
diff --git a/spec/configuration_spec.rb b/spec/configuration_spec.rb
index ef99e516..5b4fab63 100644
--- a/spec/configuration_spec.rb
+++ b/spec/configuration_spec.rb
@@ -24,52 +24,6 @@
     end
   end
 
-  # FIXME: Deprecated
-  describe "when rails is defined" do
-    let(:logger) { double(debug?: false) }
-
-    let(:rails) do
-      double(root: Pathname.new("/foo/bar"), logger: logger)
-    end
-
-    before do
-      stub_const("::Rails", rails)
-      Graphiti.instance_variable_set(:@config, nil)
-    end
-
-    after do
-      Graphiti.instance_variable_set(:@config, nil)
-    end
-
-    describe "#schema_path" do
-      it "defaults" do
-        expect(Graphiti.config.schema_path.to_s)
-          .to eq("/foo/bar/public/schema.json")
-      end
-    end
-
-    describe "#debug" do
-      subject { Graphiti.config.debug }
-
-      # FIXME: Deprecated
-      context "when rails logger is debug level" do
-        let(:logger) { double(debug?: true) }
-
-        it { is_expected.to eq(true) }
-      end
-
-      # FIXME: Deprecated
-      context "when rails logger is not debug level" do
-        it { is_expected.to eq(false) }
-      end
-    end
-
-    it "sets the graphiti logger to the rails logger" do
-      Graphiti.config
-      expect(Graphiti.logger).to eq(rails.logger)
-    end
-  end
-
   describe "#debug=" do
     it "toggles Debugger.enabled" do
       Graphiti.config.debug = true
@@ -103,20 +57,6 @@
       Graphiti.config.schema_path = "foo"
       expect(Graphiti.config.schema_path).to eq("foo")
     end
-
-    # FIXME: Deprecated
-    context "when Rails is defined" do
-      before do
-        rails = double(root: Pathname.new("/foo/bar"), logger: double.as_null_object)
-        stub_const("::Rails", rails)
-        Graphiti.instance_variable_set(:@config, nil)
-      end
-
-      it "defaults" do
-        expect(Graphiti.config.schema_path.to_s)
-          .to eq("/foo/bar/public/schema.json")
-      end
-    end
   end
 
   describe "#respond_to" do
@@ -180,6 +120,64 @@
     end
   end
 
+  describe "deprecated resource-level settings" do
+    around do |e|
+      relationship_links = Graphiti::Resource.relationship_links
+      page_links = Graphiti::Resource.page_links
+      typecast_reads = Graphiti::Resource.typecast_reads
+      e.run
+    ensure
+      Graphiti::Resource.relationship_links = relationship_links
+      Graphiti::Resource.page_links = page_links
+      Graphiti::Resource.typecast_reads = typecast_reads
+    end
+
+    describe "#typecast_reads" do
+      it "maps to Resource.typecast_reads" do
+        Graphiti.config.typecast_reads = false
+        expect(Graphiti::Resource.typecast_reads).to eq(false)
+        expect(Graphiti.config.typecast_reads).to eq(false)
+      end
+    end
+
+    describe "#links_on_demand" do
+      it "maps to Resource.relationship_links" do
+        Graphiti.config.links_on_demand = true
+        expect(Graphiti::Resource.relationship_links).to eq(:on_demand)
+        expect(Graphiti.config.links_on_demand).to eq(true)
+
+        Graphiti.config.links_on_demand = false
+        expect(Graphiti::Resource.relationship_links).to eq(true)
+        expect(Graphiti.config.links_on_demand).to eq(false)
+      end
+    end
+
+    describe "#pagination_links" do
+      it "maps to Resource.page_links" do
+        Graphiti.config.pagination_links = true
+        expect(Graphiti::Resource.page_links).to eq(true)
+        expect(Graphiti.config.pagination_links).to eq(true)
+      end
+
+      it "does not clobber :on_demand" do
+        Graphiti.config.pagination_links_on_demand = true
+        Graphiti.config.pagination_links = false
+        expect(Graphiti::Resource.page_links).to eq(:on_demand)
+      end
+    end
+
+    describe "#pagination_links_on_demand" do
+      it "maps to Resource.page_links" do
+        Graphiti.config.pagination_links_on_demand = true
+        expect(Graphiti::Resource.page_links).to eq(:on_demand)
+        expect(Graphiti.config.pagination_links_on_demand).to eq(true)
+
+        Graphiti.config.pagination_links_on_demand = false
+        expect(Graphiti::Resource.page_links).to eq(false)
+      end
+    end
+  end
+
   describe "#cache_rendering" do
     it "defaults" do
       expect(Graphiti.config.cache_rendering?).to eq(false)
diff --git a/spec/current_attributes_spec.rb b/spec/current_attributes_spec.rb
new file mode 100644
index 00000000..0915fc5c
--- /dev/null
+++ b/spec/current_attributes_spec.rb
@@ -0,0 +1,77 @@
+require "spec_helper"
+require "active_support/current_attributes"
+
+module CurrentAttributesTest
+  class Current < ActiveSupport::CurrentAttributes
+    attribute :user
+  end
+end
+
+RSpec.describe "CurrentAttributes propagation into concurrent sideloads" do
+  include ConcurrencyHarness
+
+  include_context "resource testing"
+
+  let(:resource) do
+    Class.new(PORO::EmployeeResource) do
+      def self.name
+        "PORO::EmployeeResource"
+      end
+    end
+  end
+  let(:base_scope) { {type: :employees} }
+
+  let!(:employee) { PORO::Employee.create }
+  let!(:position) { PORO::Position.create(employee_id: employee.id) }
+
+  around do |example|
+    PORO::Employee.class_eval { attr_accessor :probe_positions }
+    example.run
+  ensure
+    PORO::Employee.class_eval do
+      remove_method :probe_positions
+      remove_method :probe_positions=
+    end
+  end
+
+  before do
+    allow(Graphiti.config).to receive(:concurrency).and_return(true)
+    with_thread_pool(max_threads: 2)
+  end
+
+  after { CurrentAttributesTest::Current.reset }
+
+  it "carries Current values onto the pool thread" do
+    observed = nil
+    resource.has_many :probe_positions, resource: PORO::PositionResource, foreign_key: :employee_id do
+      scope do |employee_ids|
+        observed = {thread: Thread.current.object_id, user: CurrentAttributesTest::Current.user}
+        {type: :positions, conditions: {employee_id: employee_ids}}
+      end
+    end
+
+    CurrentAttributesTest::Current.user = "jeff"
+    # Two sideloads, because one resolves inline and never reaches the pool.
+    params[:include] = "probe_positions,positions"
+    resource.all(params).to_a
+
+    expect(observed[:thread]).to_not eq(Thread.current.object_id)
+    expect(observed[:user]).to eq("jeff")
+  end
+
+  it "keeps a sideload's writes off the request thread" do
+    resource.has_many :probe_positions, resource: PORO::PositionResource, foreign_key: :employee_id do
+      scope do |employee_ids|
+        CurrentAttributesTest::Current.user = "someone else"
+        {type: :positions, conditions: {employee_id: employee_ids}}
+      end
+    end
+
+    CurrentAttributesTest::Current.user = "jeff"
+    # Two sideloads, because one resolves inline and never reaches the pool.
+    params[:include] = "probe_positions,positions"
+    resource.all(params).to_a
+
+    expect(CurrentAttributesTest::Current.user).to eq("jeff")
+  end
+end
diff --git a/spec/deep_filter_dedup_spec.rb b/spec/deep_filter_dedup_spec.rb
new file mode 100644
index 00000000..71934d49
--- /dev/null
+++ b/spec/deep_filter_dedup_spec.rb
@@ -0,0 +1,51 @@
+require "spec_helper"
+
+RSpec.describe "deep filter vs dedup" do
+  let!(:position_resource) do
+    Class.new(PORO::ApplicationResource) do
+      def self.name
+        "RtPositionResource"
+      end
+      self.model = PORO::Position
+      self.type = :positions
+      attribute :employee_id, :integer, only: [:filterable]
+      attribute :title, :string
+      attribute :rank, :integer
+    end
+  end
+
+  let!(:employee_resource) do
+    positions = position_resource
+    klass = Class.new(PORO::ApplicationResource) do
+      def self.name
+        "RtEmployeeResource"
+      end
+      self.model = PORO::Employee
+      self.type = :employees
+      attribute :first_name, :string
+      has_many :positions, resource: positions, foreign_key: :employee_id
+    end
+    positions.belongs_to :employee, resource: klass, foreign_key: :employee_id
+    klass
+  end
+
+  before do
+    PORO::DB.clear
+    employee = PORO::Employee.create(first_name: "A")
+    PORO::Position.create(employee_id: employee.id, rank: 1, title: "one")
+    PORO::Position.create(employee_id: employee.id, rank: 2, title: "two")
+  end
+
+  def positions_for(params)
+    json = JSON.parse(employee_resource.all(params).to_jsonapi)
+    json["data"][0]["relationships"]["positions"]["data"].map { |r| r["id"] }.sort
+  end
+
+  it "narrows on the shallow path" do
+    expect(positions_for(include: "positions", filter: {"positions.rank" => 1})).to eq(["1"])
+  end
+
+  it "still narrows when the include round-trips back to positions" do
+    expect(positions_for(include: "positions.employee.positions", filter: {"positions.rank" => 1})).to eq(["1"])
+  end
+end
diff --git a/spec/deprecated_constants_spec.rb b/spec/deprecated_constants_spec.rb
new file mode 100644
index 00000000..213b9228
--- /dev/null
+++ b/spec/deprecated_constants_spec.rb
@@ -0,0 +1,135 @@
+require "graphiti_spec_helpers/rspec"
+
+RSpec.describe "deprecated constants" do
+  def silenced
+    Graphiti::DEPRECATOR.silence { yield }
+  end
+
+  it "resolves to Graphiti::SpecHelpers" do
+    # Compared inside the block. The matcher would otherwise touch the proxy
+    # after silencing has ended.
+    expect(silenced { GraphitiSpecHelpers == Graphiti::SpecHelpers }).to eq(true)
+  end
+
+  it "resolves nested constants" do
+    expect(silenced { GraphitiSpecHelpers::RSpec }).to eq(Graphiti::SpecHelpers::RSpec)
+    expect(silenced { GraphitiSpecHelpers::Sugar }).to eq(Graphiti::SpecHelpers::Sugar)
+    expect(silenced { GraphitiSpecHelpers::Node }).to eq(Graphiti::SpecHelpers::Node)
+    expect(silenced { GraphitiSpecHelpers::Errors::NoSideloads })
+      .to eq(Graphiti::SpecHelpers::Errors::NoSideloads)
+  end
+
+  it "can still be included" do
+    klass = silenced { Class.new { include GraphitiSpecHelpers } }
+
+    expect(klass.ancestors).to include(Graphiti::SpecHelpers::Helpers)
+  end
+
+  # The bare constant just hands back the proxy. It is resolving through it
+  # that reports the deprecation.
+  it "warns when used" do
+    expect(Graphiti::DEPRECATOR).to receive(:warn).at_least(:once).and_return(nil)
+
+    GraphitiSpecHelpers::Node
+  end
+
+  describe "Graphiti::SpecHelpers::Sugar" do
+    it "warns on include but the aliases still work" do
+      expect(Graphiti::DEPRECATOR).to receive(:warn).once.and_return(nil)
+
+      klass = Class.new {
+        include Graphiti::SpecHelpers::Sugar
+
+        def jsonapi_data
+          "data"
+        end
+
+        def jsonapi_errors
+          "errors"
+        end
+      }
+      instance = klass.new
+
+      expect(instance.d).to eq("data")
+      expect(instance.errors).to eq("errors")
+    end
+  end
+
+  describe "the bare shared context names" do
+    it "are still registered alongside the graphiti-prefixed ones" do
+      registry = ::RSpec.world.shared_example_group_registry.send(:shared_example_groups)[:main]
+
+      expect(registry.keys).to include(
+        "graphiti resource testing", "resource testing",
+        "graphiti remote api", "remote api"
+      )
+    end
+  end
+
+  describe "the graphiti_errors serializers" do
+    {
+      "GraphitiErrors::Validation::Serializer" => Graphiti::ErrorSerializers::Validation,
+      "GraphitiErrors::InvalidRequest::Serializer" => Graphiti::ErrorSerializers::InvalidRequest,
+      "GraphitiErrors::ConflictRequest::Serializer" => Graphiti::ErrorSerializers::ConflictRequest,
+      "GraphitiErrors::Serializers::Validation" => Graphiti::ErrorSerializers::Validation
+    }.each do |old_name, target|
+      it "resolves #{old_name}" do
+        resolved = silenced { Object.const_get(old_name) == target }
+
+        expect(resolved).to eq(true)
+      end
+    end
+
+    # These proxies are leaves. Naming one hands back the proxy, and it is
+    # calling through it that reports the deprecation.
+    it "warns when used" do
+      expect(Graphiti::DEPRECATOR).to receive(:warn).at_least(:once).and_return(nil)
+
+      GraphitiErrors::Validation::Serializer.new(Object.new)
+    end
+
+    # The mixin and its exception handlers are replaced by rescue_registry, not
+    # renamed, so there is nothing for them to point at.
+    it "does not resurrect the exception handlers" do
+      expect(defined?(GraphitiErrors::ExceptionHandler)).to be_nil
+    end
+
+    it "still answers #rendered_errors with the payload" do
+      serializer = Graphiti::ErrorSerializers::InvalidRequest
+        .new(double(details: {}, messages: {}))
+
+      expect(Graphiti::DEPRECATOR).to receive(:deprecation_warning)
+        .with("#rendered_errors", a_string_including("#errors"))
+
+      expect(serializer.rendered_errors).to eq([])
+    end
+  end
+
+  describe "GraphitiContextProxy" do
+    it "resolves to Graphiti::SpecHelpers::ContextProxy" do
+      resolved = silenced { GraphitiContextProxy == Graphiti::SpecHelpers::ContextProxy }
+
+      expect(resolved).to eq(true)
+    end
+  end
+
+  describe "include GraphitiErrors" do
+    it "raises with the replacement rather than doing nothing" do
+      expect {
+        Class.new { include GraphitiErrors }
+      }.to raise_error(/include Graphiti::Rails::Controller/)
+    end
+  end
+
+  describe "deprecated require paths" do
+    {
+      "graphiti_spec_helpers" => "Graphiti::SpecHelpers",
+      "graphiti_errors" => "Graphiti::ErrorSerializers::Validation"
+    }.each do |path, still_available|
+      it "#{path.inspect} still loads" do
+        expect { silenced { require path } }.to_not raise_error
+        expect(Object.const_defined?(still_available)).to eq(true)
+      end
+    end
+  end
+end
diff --git a/spec/deserializer_spec.rb b/spec/deserializer_spec.rb
index 803ec5e9..2757fca3 100644
--- a/spec/deserializer_spec.rb
+++ b/spec/deserializer_spec.rb
@@ -51,14 +51,14 @@
           relationships: {
             positions: {
               data: [
-                {'temp-id': "abc123", type: "positions", method: "create"}
+                {"temp-id": "abc123", type: "positions", method: "create"}
               ]
             }
           }
         },
         included: [
           {
-            'temp-id': "abc123",
+            "temp-id": "abc123",
             type: "positions",
             attributes: {title: "specialist"}
           }
@@ -99,12 +99,12 @@
         payload[:included][0][:relationships] = {
           department: {
             data: {
-              type: "departments", 'temp-id': "def456", method: "create"
+              type: "departments", "temp-id": "def456", method: "create"
             }
           }
         }
         payload[:included] << {
-          'temp-id': "def456",
+          "temp-id": "def456",
           type: "departments",
           attributes: {name: "safety"}
         }
@@ -148,21 +148,21 @@
           relationships: {
             positions: {
               data: [
-                {'temp-id': "abc123", type: "positions", method: "create"},
-                {'temp-id': "ghi789", type: "positions", method: "create"}
+                {"temp-id": "abc123", type: "positions", method: "create"},
+                {"temp-id": "ghi789", type: "positions", method: "create"}
               ]
             }
           }
         },
         included: [
           {
-            'temp-id': "abc123",
+            "temp-id": "abc123",
             type: "positions",
             attributes: {title: "pos 1"},
             relationships: {
               department: {
                 data: {
-                  'temp-id': "def456",
+                  "temp-id": "def456",
                   type: "departments",
                   method: "create"
                 }
@@ -171,13 +171,13 @@
           },
           {
             type: "positions",
-            'temp-id': "ghi789",
+            "temp-id": "ghi789",
             method: "create",
             attributes: {title: "pos 2"},
             relationships: {
               department: {
                 data: {
-                  'temp-id': "d3p2",
+                  "temp-id": "d3p2",
                   method: "create",
                   type: "departments"
                 }
@@ -186,33 +186,33 @@
           },
           {
             type: "departments",
-            'temp-id': "def456",
+            "temp-id": "def456",
             method: "create",
             attributes: {name: "safety"},
             relationships: {
               tags: {
                 data: [
-                  {'temp-id': "t4g1", type: "tags", method: "create"}
+                  {"temp-id": "t4g1", type: "tags", method: "create"}
                 ]
               }
             }
           },
           {
             type: "departments",
-            'temp-id': "d3p1",
+            "temp-id": "d3p1",
             method: "create",
             attributes: {name: "another dept"},
             relationships: {
               tags: {
                 data: [
-                  {'temp-id': "t4g1", type: "tags", method: "destroy"}
+                  {"temp-id": "t4g1", type: "tags", method: "destroy"}
                 ]
               }
             }
           },
           {
             type: "tags",
-            'temp-id': "t4g1",
+            "temp-id": "t4g1",
             attributes: {name: "foo"}
           }
         ]
diff --git a/spec/error_serializers_spec.rb b/spec/error_serializers_spec.rb
new file mode 100644
index 00000000..b4f4c114
--- /dev/null
+++ b/spec/error_serializers_spec.rb
@@ -0,0 +1,129 @@
+RSpec.describe Graphiti::ErrorSerializers do
+  describe Graphiti::ErrorSerializers::InvalidRequest do
+    let(:source) do
+      Graphiti::Util::SimpleErrors.new(Object.new).tap do |errors|
+        errors.add(:"filter.title", :unsupported, message: "is not supported")
+      end
+    end
+
+    subject(:errors) { described_class.new(source).errors }
+
+    it "renders one error per message" do
+      source.add(:"sort.name", :unsupported, message: "is not supported")
+
+      expect(errors.length).to eq(2)
+    end
+
+    it "reports the code, status and title" do
+      expect(errors[0]).to include(
+        code: "bad_request",
+        status: "400",
+        title: "Request Error"
+      )
+    end
+
+    it "takes its title from a locale key named after the code" do
+      original = I18n.backend
+      I18n.backend = I18n::Backend::Simple.new
+      I18n.backend.store_translations(:en, graphiti: {errors: {bad_request: {title: "Equestray Errorway"}}})
+
+      expect(errors[0][:title]).to eq("Equestray Errorway")
+    ensure
+      I18n.backend = original
+    end
+
+    it "turns the attribute path into a JSON pointer" do
+      source.add(:"filter.tags[0]", :unsupported, message: "is not supported")
+
+      expect(errors.map { |error| error[:source][:pointer] })
+        .to eq(["filter/title", "filter/tags/0"])
+    end
+
+    it "carries the attribute, message and code in meta" do
+      expect(errors[0][:meta]).to eq(
+        attribute: :"filter.title",
+        message: "is not supported",
+        code: :unsupported
+      )
+    end
+  end
+
+  describe Graphiti::ErrorSerializers::ConflictRequest do
+    let(:source) do
+      Graphiti::Util::SimpleErrors.new(Object.new).tap do |errors|
+        errors.add(:id, :conflict, message: "does not match")
+      end
+    end
+
+    subject(:error) { described_class.new(source).errors[0] }
+
+    it "reports a conflict in the status, code and title alike" do
+      expect(error).to include(
+        code: "conflict",
+        status: "409",
+        title: "Conflict Error"
+      )
+    end
+
+    it "describes the failure the same way InvalidRequest does" do
+      invalid = Graphiti::ErrorSerializers::InvalidRequest.new(source).errors[0]
+
+      expect(error[:detail]).to eq(invalid[:detail])
+      expect(error[:source]).to eq(invalid[:source])
+      expect(error[:meta]).to eq(invalid[:meta])
+    end
+  end
+
+  describe Graphiti::ErrorSerializers::Validation do
+    let(:object) do
+      Class.new do
+        include ActiveModel::Validations
+
+        attr_accessor :name
+
+        def self.name
+          "Author"
+        end
+
+        validates :name, presence: true
+      end.new
+    end
+
+    subject(:errors) { described_class.new(object).errors }
+
+    before { object.valid? }
+
+    it "renders a 422 validation error per failure" do
+      expect(errors).to match([a_hash_including(
+        code: "unprocessable_entity",
+        status: "422",
+        title: "Validation Error",
+        detail: "Name can't be blank",
+        source: {pointer: "/data/attributes/name"}
+      )])
+    end
+
+    it "carries the attribute, message and code in meta" do
+      expect(errors[0][:meta]).to eq(
+        attribute: :name,
+        message: "can't be blank",
+        code: :blank
+      )
+    end
+
+    it "takes its title from a locale key named after the code" do
+      original = I18n.backend
+      I18n.backend = I18n::Backend::Simple.new
+      I18n.backend.store_translations(:en, graphiti: {errors: {unprocessable_entity: {title: "Nope"}}})
+
+      expect(errors[0][:title]).to eq("Nope")
+      expect(errors[0][:code]).to eq("unprocessable_entity")
+    ensure
+      I18n.backend = original
+    end
+
+    it "is empty for an object that cannot have errors" do
+      expect(described_class.new(Object.new).errors).to eq([])
+    end
+  end
+end
diff --git a/spec/filtering_spec.rb b/spec/filtering_spec.rb
index 5600fef0..7825baf1 100644
--- a/spec/filtering_spec.rb
+++ b/spec/filtering_spec.rb
@@ -285,10 +285,30 @@ def self.name
     end
   end
 
-  context "when passed null and filter marked allow_nil: true" do
+  context "when the filter uses the deprecated blank options" do
+    it "maps allow_nil to :null" do
+      resource.filter :first_name, allow_nil: true
+      expect(resource.filters[:first_name][:blanks]).to eq(:null)
+    end
+
+    it "maps deny_empty to :rejected" do
+      resource.filter :first_name, deny_empty: true
+      expect(resource.filters[:first_name][:blanks]).to eq(:rejected)
+    end
+  end
+
+  context "when the filter is given an unknown blanks value" do
+    it "raises" do
+      expect {
+        resource.filter :first_name, blanks: :whatever
+      }.to raise_error(Graphiti::Errors::InvalidFilterBlanks, /must be one of :literal, :null, or :rejected/)
+    end
+  end
+
+  context "when passed null and filter marked blanks: :null" do
     context "with string type" do
       before do
-        resource.filter :first_name, allow_nil: true
+        resource.filter :first_name, blanks: :null
         employee2.update_attributes(first_name: nil)
         params[:filter] = {first_name: "null"}
       end
@@ -312,7 +332,7 @@ def self.name
     context "with integer type" do
       before do
         resource.attribute :age, :integer
-        resource.filter :age, allow_nil: true
+        resource.filter :age, blanks: :null
         employee1.update_attributes(age: 20)
         employee2.update_attributes(age: nil)
         employee3.update_attributes(age: 30)
@@ -338,9 +358,9 @@ def self.name
     end
   end
 
-  context "when passed an empty value when deny_empty is true" do
+  context "when passed a blank value and blanks is :rejected" do
     before do
-      resource.filter :first_name, deny_empty: true
+      resource.filter :first_name, blanks: :rejected
       employee2.update_attributes(first_name: value)
       params[:filter] = {first_name: "null"}
     end
@@ -468,14 +488,14 @@ def self.name
       before do
         params[:filter] = {
           id: employee1.id,
-          'positions.title': "bar"
+          "positions.title": "bar"
         }
         params[:include] = "positions"
       end
 
       it "works" do
         render
-        sl = d[0].sideload(:positions)
+        sl = jsonapi_data[0].sideload(:positions)
         expect(sl.map(&:id)).to eq([pos2.id])
       end
     end
@@ -495,14 +515,14 @@ def self.name
       before do
         params[:filter] = {
           id: employee1.id,
-          'positions.department.name': "bar"
+          "positions.department.name": "bar"
         }
         params[:include] = "positions.department"
       end
 
       it "works" do
         render
-        positions = d[0].sideload(:positions)
+        positions = jsonapi_data[0].sideload(:positions)
         expect(positions[0].sideload(:department)).to be_nil
         expect(positions[1].sideload(:department).id).to eq(department2.id)
       end
@@ -518,7 +538,7 @@ def self.name
 
         it "works" do
           render
-          positions = d[0].sideload(:positions)
+          positions = jsonapi_data[0].sideload(:positions)
           expect(positions[0].sideload(:department).id).to eq(department2.id)
           expect(positions[1].sideload(:department)).to be_nil
         end
@@ -535,7 +555,7 @@ def self.name
 
         it "works" do
           render
-          positions = d[0].sideload(:positions)
+          positions = jsonapi_data[0].sideload(:positions)
           expect(positions.map(&:id)).to eq([2])
           expect(positions[0].sideload(:department).id).to eq(department2.id)
         end
@@ -972,7 +992,7 @@ def assert_filter_value(value)
 
       it "coerces integers" do
         params[:filter] = {foo: 40}
-        assert_filter_value([BigDecimal("40")])
+        assert_filter_value([BigDecimal(40)])
       end
 
       it "coerces strings" do
diff --git a/spec/fixtures/employee_directory.rb b/spec/fixtures/employee_directory.rb
index d074b61a..f33d7655 100644
--- a/spec/fixtures/employee_directory.rb
+++ b/spec/fixtures/employee_directory.rb
@@ -47,6 +47,7 @@
   create_table :positions do |t|
     t.belongs_to :department, index: true
     t.belongs_to :employee, index: true
+    t.string :region_name
     t.string :title
   end
 
@@ -54,6 +55,12 @@
     t.string :name
   end
 
+  # Keyed by a string rather than an integer :id, so a belongs_to pointing at
+  # it has a foreign key that is not the related resource's rendered id.
+  create_table :regions, primary_key: :code, id: :string do |t|
+    t.string :name
+  end
+
   create_table :salaries do |t|
     t.integer :employee_id
     t.decimal :base_rate
@@ -154,12 +161,18 @@ class Employee < ApplicationRecord
 class Position < ApplicationRecord
   belongs_to :employee
   belongs_to :department
+  belongs_to :region, foreign_key: :region_name, primary_key: :name, optional: true
 end
 
 class Department < ApplicationRecord
   has_many :positions
 end
 
+class Region < ApplicationRecord
+  self.primary_key = "code"
+  has_many :positions, foreign_key: :region_name, primary_key: :name
+end
+
 class Salary < ApplicationRecord
   belongs_to :employee
 end
@@ -184,6 +197,10 @@ class DepartmentResource < ApplicationResource
   attribute :name, :string
 end
 
+class RegionResource < ApplicationResource
+  attribute :name, :string
+end
+
 class PositionResource < ApplicationResource
   attribute :employee_id, :integer, only: [:writable, :filterable]
   attribute :title, :string
diff --git a/spec/fixtures/legacy.rb b/spec/fixtures/legacy.rb
index 15a97f54..9bd14d62 100644
--- a/spec/fixtures/legacy.rb
+++ b/spec/fixtures/legacy.rb
@@ -139,11 +139,11 @@ class Author < ApplicationRecord
       class_name: "Legacy::State"
 
     has_many :mentor_joins, class_name: "AuthorMentorship",
-                            foreign_key: :mentee_id, inverse_of: :mentee
+      foreign_key: :mentee_id, inverse_of: :mentee
     has_many :mentors, through: :mentor_joins, class_name: "Author", source: :mentor
 
     has_many :mentee_joins, class_name: "AuthorMentorship",
-                            foreign_key: :mentor_id, inverse_of: :mentor
+      foreign_key: :mentor_id, inverse_of: :mentor
     has_many :mentees, through: :mentee_joins, class_name: "Author", source: :mentee
   end
 
@@ -381,7 +381,7 @@ class AuthorResource < ApplicationResource
     attribute :created_at_date, :date, only: [:filterable]
     attribute :identifier, :uuid
 
-    filter :last_login, allow_nil: true
+    filter :last_login, blanks: :null
 
     has_many :books
     belongs_to :state
diff --git a/spec/fixtures/poro.rb b/spec/fixtures/poro.rb
index f7d0d64e..5ba71f6b 100644
--- a/spec/fixtures/poro.rb
+++ b/spec/fixtures/poro.rb
@@ -51,7 +51,7 @@ def klasses
 
       def all(params)
         target_types = params[:type]
-        records = data.select { |k, v| Array(target_types).include?(k) }
+        records = data.slice(*Array(target_types))
         return [] unless records
         records = records.map { |type, records_for_type|
           records_for_type.map { |attrs| klasses[type].new(attrs) }
@@ -101,7 +101,7 @@ def apply_pagination(records, params)
         return records unless params[:per]
         records = records[params[:offset]..records.length] if params[:offset]
 
-        start_at = (params[:page] - 1) * (params[:per])
+        start_at = (params[:page] - 1) * params[:per]
         end_at = (params[:page] * params[:per]) - 1
         return [] if end_at < 0
         records[start_at..end_at]
@@ -111,6 +111,7 @@ def apply_pagination(records, params)
 
   class Base
     include ActiveModel::Validations
+
     attr_accessor :id
 
     def self.create(attrs = {})
diff --git a/spec/graphiti_spec.rb b/spec/graphiti_spec.rb
index 0d2f5807..5378df0a 100644
--- a/spec/graphiti_spec.rb
+++ b/spec/graphiti_spec.rb
@@ -19,4 +19,33 @@
       described_class.setup!
     end
   end
+
+  describe ".context" do
+    it "reads :namespace as a deprecated alias of :action" do
+      described_class.with_context(double, :index) do
+        expect(Graphiti::DEPRECATOR).to receive(:deprecation_warning)
+          .with(:"context[:namespace]", /current_action/, anything)
+        expect(described_class.context[:namespace]).to eq(:index)
+      end
+    end
+
+    it "writes :namespace through to :action" do
+      described_class.with_context(double, :index) do
+        allow(Graphiti::DEPRECATOR).to receive(:deprecation_warning)
+        described_class.context[:namespace] = :update
+        expect(described_class.context[:action]).to eq(:update)
+      end
+    end
+
+    it "is visible to a fiber started within the request" do
+      object = double
+      seen = nil
+
+      described_class.with_context(object, :index) do
+        Fiber.new { seen = described_class.context[:object] }.resume
+      end
+
+      expect(seen).to eq(object)
+    end
+  end
 end
diff --git a/spec/integration/rails/activerecord_to_poro_spec.rb b/spec/integration/rails/activerecord_to_poro_spec.rb
index 05044d7d..038bf172 100644
--- a/spec/integration/rails/activerecord_to_poro_spec.rb
+++ b/spec/integration/rails/activerecord_to_poro_spec.rb
@@ -1,6 +1,6 @@
 if ENV["APPRAISAL_INITIALIZED"]
   RSpec.describe "associating an ActiveRecord to a PORO", type: :controller do
-    include GraphitiSpecHelpers
+    include Graphiti::SpecHelpers
 
     module ARToPORO
       class AuthorResource < Legacy::ApplicationResource
@@ -46,7 +46,7 @@ def index
     context "when has_many" do
       it "works" do
         do_index({include: "books"})
-        sl = d[0].sideload(:books)
+        sl = jsonapi_data[0].sideload(:books)
         expect(sl.map(&:id)).to eq([book.id])
         expect(sl[0].jsonapi_type).to eq("books")
       end
@@ -55,7 +55,7 @@ def index
     context "when belongs_to" do
       it "works" do
         do_index({include: "state"})
-        sl = d[0].sideload(:state)
+        sl = jsonapi_data[0].sideload(:state)
         expect(sl.id).to eq(state.id)
         expect(sl.jsonapi_type).to eq("states")
       end
diff --git a/spec/integration/rails/ar_dedup_spec.rb b/spec/integration/rails/ar_dedup_spec.rb
new file mode 100644
index 00000000..4e7a9f3e
--- /dev/null
+++ b/spec/integration/rails/ar_dedup_spec.rb
@@ -0,0 +1,78 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  require "rails_spec_helper"
+
+  module DedupTest
+    DB_PATH = File.expand_path("../../tmp/dedup_probe.sqlite3", __dir__)
+  end
+
+  # Two sideloads write to the same record from different threads. That only works because Ruby runs one thread at a time, so this test fails if that ever changes.
+  RSpec.describe "dedup against ActiveRecord's association cache" do
+    include ConcurrencyHarness
+
+    before(:all) do
+      base = FileBackedDatabase.connect(DedupTest, DedupTest::DB_PATH, pool: 10)
+
+      base.connection.create_table(:dedup_parents, force: true) { |t| t.string :name }
+      base.connection.create_table(:dedup_children, force: true) do |t|
+        t.integer :dedup_parent_id
+        t.string :kind
+      end
+
+      DedupTest.const_set(:Child, Class.new(base) {
+        self.table_name = "dedup_children"
+      })
+      DedupTest.const_set(:Parent, Class.new(base) {
+        self.table_name = "dedup_parents"
+        has_many :alphas, -> { where(kind: "alpha") }, class_name: "DedupTest::Child", foreign_key: :dedup_parent_id
+        has_many :betas, -> { where(kind: "beta") }, class_name: "DedupTest::Child", foreign_key: :dedup_parent_id
+      })
+
+      DedupTest.const_set(:ChildResource, Class.new(Graphiti::Resource) {
+        self.adapter = Graphiti::Adapters::ActiveRecord
+        self.model = DedupTest::Child
+        self.type = :dedup_children
+        attribute :dedup_parent_id, :integer, only: [:filterable]
+        attribute :kind, :string
+      })
+      DedupTest.const_set(:ParentResource, Class.new(Graphiti::Resource) {
+        self.adapter = Graphiti::Adapters::ActiveRecord
+        self.model = DedupTest::Parent
+        self.type = :dedup_parents
+        attribute :name, :string
+        has_many :alphas, resource: DedupTest::ChildResource, foreign_key: :dedup_parent_id
+        has_many :betas, resource: DedupTest::ChildResource, foreign_key: :dedup_parent_id
+      })
+
+      60.times do |index|
+        parent = DedupTest::Parent.create!(name: "p#{index}")
+        2.times { DedupTest::Child.create!(dedup_parent_id: parent.id, kind: "alpha") }
+        2.times { DedupTest::Child.create!(dedup_parent_id: parent.id, kind: "beta") }
+      end
+    end
+
+    after(:all) do
+      FileBackedDatabase.disconnect(DedupTest, DedupTest::DB_PATH,
+        %i[ParentResource ChildResource Child Parent])
+    end
+
+    before do
+      allow(Graphiti.config).to receive(:concurrency).and_return(true)
+      with_thread_pool(max_threads: 4)
+    end
+
+    it "keeps both sibling associations on every shared instance" do
+      damaged = []
+
+      8.times do |run|
+        parents = DedupTest::ParentResource.all(page: {size: 60}, include: "alphas,betas").to_a
+        parents.each do |parent|
+          cache = parent.instance_variable_get(:@association_cache) || {}
+          loaded = cache.keys.sort
+          damaged << [run, parent.id, loaded] unless loaded == [:alphas, :betas]
+        end
+      end
+
+      expect(damaged).to be_empty, "lost associations on #{damaged.size} parents, e.g. #{damaged.first(3).inspect}"
+    end
+  end
+end
diff --git a/spec/integration/rails/belongs_to_resource_ids_spec.rb b/spec/integration/rails/belongs_to_resource_ids_spec.rb
new file mode 100644
index 00000000..3c7ba6a0
--- /dev/null
+++ b/spec/integration/rails/belongs_to_resource_ids_spec.rb
@@ -0,0 +1,202 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  RSpec.describe "belongs_to resource linkage" do
+    include Graphiti::SpecHelpers
+
+    let!(:employee) { Employee.create!(first_name: "Jane") }
+    let!(:position) do
+      Position.create!(employee: employee, title: "Engineer")
+    end
+
+    def queries_while
+      queries = []
+      subscriber = ActiveSupport::Notifications
+        .subscribe("sql.active_record") do |_, _, _, _, payload|
+          sql = payload[:sql]
+          next if payload[:name].to_s == "SCHEMA"
+          next if sql.start_with?("PRAGMA", "begin", "commit")
+          queries << sql
+        end
+      yield
+      queries
+    ensure
+      ActiveSupport::Notifications.unsubscribe(subscriber)
+    end
+
+    # Filter to the record under test: examples in this file share a database.
+    def linkage_for(resource_class, record = position)
+      json = JSON.parse(resource_class.all(filter: {id: record.id}).to_jsonapi)
+      json["data"][0]["relationships"]["employee"]
+    end
+
+    context "when the relationship is a plain belongs_to" do
+      let(:resource_class) do
+        Class.new(PositionResource) do
+          def self.name
+            "PositionResource"
+          end
+
+          belongs_to :employee, resource_ids: true
+        end
+      end
+
+      it "renders the linkage" do
+        expect(linkage_for(resource_class)).to eq(
+          "data" => {"type" => "employees", "id" => employee.id.to_s}
+        )
+      end
+
+      it "does not query employees to do it" do
+        queries = queries_while { linkage_for(resource_class) }
+
+        expect(queries.grep(/FROM .employees./)).to be_empty
+      end
+
+      it "renders null when the foreign key is nil" do
+        # update_column, not update!: the loaded belongs_to association writes
+        # its id back over the nil on save.
+        position.update_column(:employee_id, nil)
+        expect(linkage_for(resource_class)).to eq("data" => nil)
+      end
+
+      it "still renders correct linkage when the relationship is included" do
+        json = JSON.parse(
+          resource_class.all(
+            filter: {id: position.id}, include: "employee"
+          ).to_jsonapi
+        )
+
+        expect(json["data"][0]["relationships"]["employee"]["data"]).to eq(
+          "type" => "employees", "id" => employee.id.to_s
+        )
+        expect(json["included"].map { |r| r["type"] }).to include("employees")
+      end
+
+      it "matches what loading the association produces" do
+        from_foreign_key = linkage_for(resource_class)
+
+        loaded = Class.new(PositionResource) do
+          def self.name
+            "PositionResource"
+          end
+
+          belongs_to :employee, resource_ids: true
+        end
+        loaded.sideloads[:employee]
+          .define_singleton_method(:resource_ids_from_foreign_key?) { false }
+
+        expect(from_foreign_key).to eq(linkage_for(loaded))
+      end
+    end
+
+    context "when the base scope does not select the foreign key" do
+      let(:resource_class) do
+        Class.new(PositionResource) do
+          def self.name
+            "PositionResource"
+          end
+
+          def base_scope
+            Position.select(:id, :title)
+          end
+
+          belongs_to :employee, resource_ids: true
+        end
+      end
+
+      it "raises an error naming the opt-out" do
+        expect {
+          linkage_for(resource_class)
+        }.to raise_error(Graphiti::Errors::UnselectedForeignKey, /resource_ids: false/)
+      end
+    end
+
+    context "when the relationship could resolve to a different record" do
+      def sideload_for(&blk)
+        klass = Class.new(PositionResource) do
+          def self.name
+            "PositionResource"
+          end
+        end
+        klass.instance_eval(&blk)
+        klass.sideloads[:employee]
+      end
+
+      it "loads the association when a params block is present" do
+        sideload = sideload_for do
+          belongs_to :employee do
+            params do |hash|
+              hash[:filter][:active] = true
+            end
+          end
+        end
+
+        expect(sideload.resource_ids_from_foreign_key?).to eq(false)
+      end
+
+      it "loads the association when a base_scope is present" do
+        sideload = sideload_for do
+          belongs_to :employee, base_scope: -> { Employee.all }
+        end
+
+        expect(sideload.resource_ids_from_foreign_key?).to eq(false)
+      end
+
+      it "loads the association when the target resource is polymorphic" do
+        sideload = sideload_for do
+          belongs_to :employee, resource: TaskResource
+        end
+
+        expect(sideload.resource_ids_from_foreign_key?).to eq(false)
+      end
+    end
+
+    context "when the target is keyed by something other than :id" do
+      # Examples in this file share a database, so this may already exist.
+      let!(:region) do
+        Region.find_or_create_by!(code: "rg-1") { |r| r.name = "Northeast" }
+      end
+
+      before { position.update_column(:region_name, "Northeast") }
+
+      let(:resource_class) do
+        Class.new(PositionResource) do
+          def self.name
+            "PositionResource"
+          end
+
+          belongs_to :region,
+            resource: RegionResource,
+            foreign_key: :region_name,
+            primary_key: :name,
+            resource_ids: true
+        end
+      end
+
+      def region_linkage
+        json = JSON.parse(
+          resource_class.all(filter: {id: position.id}).to_jsonapi
+        )
+        json["data"][0]["relationships"]["region"]
+      end
+
+      it "renders the related id, not the foreign key" do
+        expect(region_linkage["data"]).to eq(
+          "type" => "regions", "id" => region.id.to_s
+        )
+      end
+
+      it "loads the association to get it" do
+        queries = queries_while { region_linkage }
+
+        expect(queries.grep(/FROM .regions./)).to_not be_empty
+      end
+    end
+
+    context "for relationship types other than belongs_to" do
+      it "never derives linkage from a foreign key" do
+        expect(EmployeeResource.sideloads[:positions].resource_ids_from_foreign_key?)
+          .to eq(false)
+      end
+    end
+  end
+end
diff --git a/spec/integration/rails/cache_key_spec.rb b/spec/integration/rails/cache_key_spec.rb
new file mode 100644
index 00000000..f0de12dd
--- /dev/null
+++ b/spec/integration/rails/cache_key_spec.rb
@@ -0,0 +1,193 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  require "rails_spec_helper"
+
+  RSpec.describe "cache keys over a resolved graph" do
+    include ConcurrencyHarness
+
+    before(:all) do
+      @db_path = File.expand_path("../../tmp/cache_key_probe.sqlite3", __dir__)
+      FileUtils.mkdir_p(File.dirname(@db_path))
+      FileUtils.rm_f(@db_path)
+      Object.const_set(:CacheRecord, Class.new(ActiveRecord::Base) { self.abstract_class = true })
+      CacheRecord.establish_connection(adapter: "sqlite3", database: @db_path, pool: 10, timeout: 5000)
+
+      CacheRecord.connection.create_table(:cache_parents, force: true) { |t|
+        t.string :name
+        t.timestamps
+      }
+      CacheRecord.connection.create_table(:cache_children, force: true) do |t|
+        t.integer :cache_parent_id
+        t.string :name
+        t.timestamps
+      end
+      CacheRecord.connection.create_table(:cache_grandchildren, force: true) do |t|
+        t.integer :cache_child_id
+        t.string :name
+        t.timestamps
+      end
+
+      Object.const_set(:CacheGrandchild, Class.new(CacheRecord) { self.table_name = "cache_grandchildren" })
+      Object.const_set(:CacheChild, Class.new(CacheRecord) {
+        self.table_name = "cache_children"
+        has_many :cache_grandchildren, class_name: "CacheGrandchild", foreign_key: :cache_child_id
+      })
+      Object.const_set(:CacheParent, Class.new(CacheRecord) {
+        self.table_name = "cache_parents"
+        has_many :cache_children, class_name: "CacheChild", foreign_key: :cache_parent_id
+        has_many :ghosts, class_name: "CacheChild", foreign_key: :cache_parent_id
+      })
+
+      Object.const_set(:CacheGrandchildResource, Class.new(Graphiti::Resource) {
+        self.adapter = Graphiti::Adapters::ActiveRecord
+        self.model = CacheGrandchild
+        self.type = :cache_grandchildren
+        attribute :cache_child_id, :integer, only: [:filterable]
+        attribute :name, :string
+      })
+      Object.const_set(:CacheChildResource, Class.new(Graphiti::Resource) {
+        self.adapter = Graphiti::Adapters::ActiveRecord
+        self.model = CacheChild
+        self.type = :cache_children
+        attribute :cache_parent_id, :integer, only: [:filterable]
+        attribute :name, :string
+        has_many :cache_grandchildren,
+          resource: CacheGrandchildResource,
+          foreign_key: :cache_child_id
+      })
+      Object.const_set(:CacheParentResource, Class.new(Graphiti::Resource) {
+        self.adapter = Graphiti::Adapters::ActiveRecord
+        self.model = CacheParent
+        self.type = :cache_parents
+        attribute :name, :string
+        has_many :cache_children,
+          resource: CacheChildResource,
+          foreign_key: :cache_parent_id
+
+        # Assigns nothing back to its parents, so the association reader on a
+        # parent lazy-loads instead of returning what the sideload resolved.
+        has_many :ghosts, resource: CacheChildResource, foreign_key: :cache_parent_id do
+          assign do |parents, children|
+            children.each(&:name)
+          end
+        end
+      })
+
+      3.times do |i|
+        parent = CacheParent.create!(name: "p#{i}")
+        2.times do |j|
+          child = CacheChild.create!(cache_parent_id: parent.id, name: "c#{i}#{j}")
+          2.times { |k| CacheGrandchild.create!(cache_child_id: child.id, name: "g#{i}#{j}#{k}") }
+        end
+      end
+    end
+
+    after(:all) do
+      CacheRecord.remove_connection
+      FileUtils.rm_f(@db_path)
+      %i[
+        CacheParentResource CacheChildResource CacheGrandchildResource
+        CacheParent CacheChild CacheGrandchild CacheRecord
+      ].each do |name|
+        Object.send(:remove_const, name) if Object.const_defined?(name)
+      end
+    end
+
+    def proxy(include:)
+      CacheParentResource.all(page: {size: 10}, include: include)
+    end
+
+    def etag_after_render(include:)
+      rendered = proxy(include: include)
+      rendered.to_jsonapi
+      rendered.cache_key_with_version
+    end
+
+    def etag_without_render(include:)
+      proxy(include: include).cache_key_with_version
+    end
+
+    [false, true].each do |concurrency|
+      context "with concurrency #{concurrency}" do
+        before do
+          allow(Graphiti.config).to receive(:concurrency).and_return(concurrency)
+          with_thread_pool(max_threads: 4) if concurrency
+        end
+
+        it "computes the same key whether or not the graph was resolved first" do
+          expect(etag_after_render(include: "cache_children.cache_grandchildren"))
+            .to eq(etag_without_render(include: "cache_children.cache_grandchildren"))
+        end
+
+        it "changes the key when a sideloaded record changes" do
+          before_touch = etag_after_render(include: "cache_children")
+          CacheChild.first.touch
+          expect(etag_after_render(include: "cache_children")).not_to eq(before_touch)
+        end
+
+        it "changes the key when a record two levels down changes" do
+          before_touch = etag_after_render(include: "cache_children.cache_grandchildren")
+          CacheGrandchild.last.touch
+          expect(etag_after_render(include: "cache_children.cache_grandchildren")).not_to eq(before_touch)
+        end
+
+        it "changes the key when a sideload whose assign writes nothing back changes" do
+          before_touch = etag_after_render(include: "ghosts")
+          CacheChild.last.touch
+          expect(etag_after_render(include: "ghosts")).not_to eq(before_touch)
+        end
+
+        it "reports the latest updated_at across the graph" do
+          rendered = proxy(include: "cache_children.cache_grandchildren")
+          rendered.to_jsonapi
+          latest = CacheGrandchild.last
+          latest.update!(updated_at: 1.day.from_now)
+
+          expect(proxy(include: "cache_children.cache_grandchildren").updated_at.to_i)
+            .to eq(latest.reload.updated_at.to_i)
+        end
+      end
+    end
+
+    describe "resolutions" do
+      # Prepended once: a module per example would stack and multiply the counts.
+      before(:all) do
+        Graphiti::Adapters::ActiveRecord.prepend(Module.new do
+          def resolve(scope)
+            count = Thread.current[:cache_key_resolves]
+            Thread.current[:cache_key_resolves] = count + 1 if count
+            super
+          end
+        end)
+      end
+
+      def resolves
+        Thread.current[:cache_key_resolves] = 0
+        yield
+        Thread.current[:cache_key_resolves]
+      ensure
+        Thread.current[:cache_key_resolves] = nil
+      end
+
+      before { allow(Graphiti.config).to receive(:concurrency).and_return(false) }
+
+      {
+        "cache_children" => 2,
+        "cache_children.cache_grandchildren" => 3
+      }.each_pair do |include, expected|
+        it "resolves #{expected} times for include=#{include}, with or without a cache key" do
+          render_only = resolves { proxy(include: include).to_jsonapi }
+
+          with_cache_key = resolves do
+            rendered = proxy(include: include)
+            rendered.to_jsonapi
+            rendered.cache_key_with_version
+            rendered.updated_at
+          end
+
+          expect(render_only).to eq(expected)
+          expect(with_cache_key).to eq(expected)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/integration/rails/callbacks_spec.rb b/spec/integration/rails/callbacks_spec.rb
index f08a74b7..065e337f 100644
--- a/spec/integration/rails/callbacks_spec.rb
+++ b/spec/integration/rails/callbacks_spec.rb
@@ -8,6 +8,7 @@
 
       routes.draw do
         post "create" => "anonymous#create"
+        put "update" => "anonymous#update"
         delete "destroy" => "anonymous#destroy"
       end
 
@@ -34,6 +35,7 @@ class ApplicationResource < Graphiti::Resource
 
       class EmployeeResource < ApplicationResource
         self.model = Employee
+        self.type = "employees"
 
         before_attributes :one
         before_attributes :two
@@ -168,6 +170,18 @@ def create
         end
       end
 
+      def update
+        employee = IntegrationCallbacks::EmployeeResource._find(params)
+        Thread.current[:proxy] = employee
+        employee.assign_attributes(params)
+
+        if employee.update_attributes
+          render jsonapi: employee
+        else
+          raise "whoops"
+        end
+      end
+
       def destroy
         employee = IntegrationCallbacks::EmployeeResource._find(params)
         Thread.current[:proxy] = employee
@@ -227,6 +241,39 @@ def params
         end
       end
 
+      describe "update callbacks" do
+        let!(:employee) { Employee.create!(first_name: "asdf") }
+        let(:payload) {
+          {id: employee.id,
+           data: {
+             id: employee.id,
+             type: "employees",
+             attributes: {first_name: "Jane"}
+           }}
+        }
+
+        it "fires hooks in order" do
+          expect {
+            put :update, params: payload
+          }.to change { Employee.find(employee.id).first_name }
+          employee = proxy.data
+          expect(employee.first_name)
+            .to eq("Jane5a6a7a12347b6b5b_12a_13a_14a89_10_11_14b_13b_12b")
+        end
+
+        context "when an error is raised" do
+          before do
+            $raise = true
+          end
+
+          it "rolls back the transaction" do
+            expect {
+              expect { put :update, params: payload }.to raise_error("test")
+            }.to_not(change { Employee.count })
+          end
+        end
+      end
+
       describe "destroy callbacks" do
         let!(:employee) { Employee.create!(first_name: "Jane") }
 
diff --git a/spec/integration/rails/concurrency_stress_spec.rb b/spec/integration/rails/concurrency_stress_spec.rb
index 1f1a9f78..cf5c557e 100644
--- a/spec/integration/rails/concurrency_stress_spec.rb
+++ b/spec/integration/rails/concurrency_stress_spec.rb
@@ -12,13 +12,7 @@ module StressTest
     include ConcurrencyHarness
 
     before(:all) do
-      FileUtils.mkdir_p(File.dirname(StressTest::DB_PATH))
-      FileUtils.rm_f(StressTest::DB_PATH)
-
-      StressTest.const_set(:Base, Class.new(ActiveRecord::Base) { self.abstract_class = true })
-      StressTest::Base.establish_connection(
-        adapter: "sqlite3", database: StressTest::DB_PATH, pool: StressTest::DB_POOL_SIZE, timeout: 5000
-      )
+      FileBackedDatabase.connect(StressTest, StressTest::DB_PATH, pool: StressTest::DB_POOL_SIZE)
 
       StressTest::Base.connection.create_table(:departments, force: true) do |t|
         t.string :name
@@ -53,11 +47,8 @@ module StressTest
     end
 
     after(:all) do
-      StressTest::Base.remove_connection
-      FileUtils.rm_f(StressTest::DB_PATH)
-      %i[EmployeeResource Employee Department Base].each do |name|
-        StressTest.send(:remove_const, name) if StressTest.const_defined?(name, false)
-      end
+      FileBackedDatabase.disconnect(StressTest, StressTest::DB_PATH,
+        %i[EmployeeResource Employee Department])
     end
 
     before do
diff --git a/spec/integration/rails/connection_pool_hint_spec.rb b/spec/integration/rails/connection_pool_hint_spec.rb
new file mode 100644
index 00000000..e85c2727
--- /dev/null
+++ b/spec/integration/rails/connection_pool_hint_spec.rb
@@ -0,0 +1,72 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  require "rails_spec_helper"
+  require "graphiti/rails/rake_helpers"
+
+  RSpec.describe "connection pool exhaustion inside a concurrent sideload" do
+    include ConcurrencyHarness
+
+    let!(:employee) { PORO::Employee.create }
+
+    before do
+      allow(Graphiti.config).to receive(:concurrency).and_return(true)
+      with_thread_pool(max_threads: 2)
+    end
+
+    it "points the timeout at the sizing formula" do
+      position_resource = Class.new(PORO::PositionResource) do
+        def self.name
+          "PORO::PositionResource"
+        end
+
+        def resolve(_scope)
+          raise ActiveRecord::ConnectionTimeoutError, "could not obtain a connection from the pool"
+        end
+      end
+      resource_class = Class.new(PORO::EmployeeResource) do
+        def self.name
+          "PORO::EmployeeResource"
+        end
+      end
+      resource_class.has_many :probe_positions, resource: position_resource, foreign_key: :employee_id
+      # Two sideloads, since one on its own resolves inline and never reaches the pool.
+      resource_class.has_many :other_positions, resource: position_resource, foreign_key: :employee_id
+
+      expect {
+        resource_class.all(filter: {id: employee.id}, include: "probe_positions,other_positions").to_a
+      }.to raise_error(ActiveRecord::ConnectionTimeoutError) { |error|
+        expect(error.message).to include("could not obtain a connection")
+        expect(error.message).to include("concurrency_max_threads")
+        expect(error.message).to include("concurrency-pool-sizing")
+      }
+    end
+  end
+
+  RSpec.describe Graphiti::Rails::RakeHelpers do
+    describe ".connection_pool_advisory" do
+      before do
+        allow(ENV).to receive(:fetch).and_call_original
+        allow(ENV).to receive(:fetch).with("RAILS_MAX_THREADS", 5).and_return("5")
+      end
+
+      def with_pool_size(size)
+        config = ActiveRecord::Base.connection_db_config
+        allow(ActiveRecord::Base).to receive(:connection_db_config)
+          .and_return(instance_double(config.class, pool: size))
+      end
+
+      it "warns when the pool cannot cover web and sideload threads" do
+        with_pool_size(5)
+
+        advisory = described_class.connection_pool_advisory
+        expect(advisory).to include("pool is 5")
+        expect(advisory).to include("+ 1 = 10")
+      end
+
+      it "stays quiet when the pool is big enough" do
+        with_pool_size(50)
+
+        expect(described_class.connection_pool_advisory).to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/integration/rails/context_spec.rb b/spec/integration/rails/context_spec.rb
new file mode 100644
index 00000000..530493fa
--- /dev/null
+++ b/spec/integration/rails/context_spec.rb
@@ -0,0 +1,85 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  RSpec.describe Graphiti::Rails::Context, type: :controller do
+    controller(ApplicationController) do
+      def index
+        render json: {
+          object: Graphiti.context[:object].class.name,
+          action: Graphiti.context[:action]
+        }
+      end
+    end
+
+    it "comes along with Graphiti::Rails::Controller" do
+      expect(controller).to be_a(described_class)
+    end
+
+    it "defaults the context to the controller instance" do
+      expect(controller.graphiti_context).to eq(controller)
+    end
+
+    it "wraps the action in a context of the controller and the action name" do
+      wrapped = nil
+      allow(Graphiti).to receive(:with_context).and_wrap_original do |original, object, action, &block|
+        wrapped = [object, action]
+        original.call(object, action, &block)
+      end
+
+      get :index
+
+      expect(wrapped).to eq([controller, :index])
+    end
+
+    it "exposes the context to resources for the duration of the action" do
+      get :index
+
+      body = JSON.parse(response.body)
+      expect(body["object"]).to eq(controller.class.name)
+      expect(body["action"]).to eq("index")
+    end
+
+    it "unsets the context once the action returns" do
+      get :index
+
+      expect(Graphiti.context).to eq({})
+    end
+
+    context "when the controller overrides graphiti_context" do
+      controller(ApplicationController) do
+        def index
+          render json: {object: Graphiti.context[:object]}
+        end
+
+        def graphiti_context
+          "custom"
+        end
+      end
+
+      it "wraps the action in that context instead" do
+        get :index
+
+        expect(JSON.parse(response.body)["object"]).to eq("custom")
+      end
+    end
+
+    context "when the controller overrides the deprecated jsonapi_context" do
+      controller(ApplicationController) do
+        def index
+          render json: {object: Graphiti.context[:object]}
+        end
+
+        def jsonapi_context
+          "legacy"
+        end
+      end
+
+      it "still uses it, and deprecates the override" do
+        expect(Graphiti::DEPRECATOR).to receive(:deprecation_warning)
+          .with("Overriding jsonapi_context", "Override #graphiti_context instead")
+
+        get :index
+
+        expect(JSON.parse(response.body)["object"]).to eq("legacy")
+      end
+    end
+  end
+end
diff --git a/spec/integration/rails/controller_spec.rb b/spec/integration/rails/controller_spec.rb
new file mode 100644
index 00000000..fa2c9b63
--- /dev/null
+++ b/spec/integration/rails/controller_spec.rb
@@ -0,0 +1,57 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  RSpec.describe Graphiti::Rails::Controller, type: :controller do
+    # Graphiti applied all of this to every controller in the app until 2.0.
+    # These assert the opposite: a controller that has not asked for Graphiti
+    # is left alone.
+    describe "a controller without it" do
+      controller(ActionController::Base) do
+        def index
+          render json: {context: Graphiti.context}
+        end
+      end
+
+      it "gets no graphiti context" do
+        get :index
+
+        expect(JSON.parse(response.body)["context"]).to eq({})
+      end
+
+      it "does not wrap actions in the debugger" do
+        expect(Graphiti::Debugger).to_not receive(:debug)
+
+        get :index
+      end
+
+      it "does not register Graphiti's exception handlers" do
+        registry = controller.class.rescue_registry
+
+        expect(registry.handles_exception?(Graphiti::Errors::RecordNotFound.new)).to eq(false)
+        expect(registry.handles_exception?(RuntimeError.new)).to eq(false)
+      end
+    end
+
+    describe "a controller with it" do
+      controller(ActionController::Base) do
+        include Graphiti::Rails::Controller
+
+        def index
+          render json: {object: Graphiti.context[:object].class.name}
+        end
+      end
+
+      it "gets the graphiti context" do
+        get :index
+
+        expect(JSON.parse(response.body)["object"]).to eq(controller.class.name)
+      end
+
+      it "registers Graphiti's exception handlers" do
+        registry = controller.class.rescue_registry
+
+        expect(registry.handles_exception?(Graphiti::Errors::RecordNotFound.new)).to eq(true)
+        # the Exception catch-all
+        expect(registry.handles_exception?(RuntimeError.new)).to eq(true)
+      end
+    end
+  end
+end
diff --git a/spec/integration/rails/current_attributes_spec.rb b/spec/integration/rails/current_attributes_spec.rb
new file mode 100644
index 00000000..29809ad5
--- /dev/null
+++ b/spec/integration/rails/current_attributes_spec.rb
@@ -0,0 +1,51 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  require "rails_spec_helper"
+
+  module RailsCurrentAttributesTest
+    class Current < ActiveSupport::CurrentAttributes
+      attribute :user
+    end
+  end
+
+  RSpec.describe "CurrentAttributes inside concurrent sideloads" do
+    include ConcurrencyHarness
+
+    let!(:employee) { Employee.create!(first_name: "Jane") }
+
+    before do
+      allow(Graphiti.config).to receive(:concurrency).and_return(true)
+      with_thread_pool(max_threads: 2)
+    end
+
+    after { RailsCurrentAttributesTest::Current.reset }
+
+    it "survives the executor handing the pool thread a fresh Current" do
+      observed = nil
+
+      resource_class = Class.new(EmployeeResource) do
+        def self.name
+          "EmployeeResource"
+        end
+      end
+      # Position.none, because the pool thread's connection does not see the
+      # in-memory test schema. The scope block still runs where a real one would.
+      resource_class.has_many :probe_positions, resource: PositionResource, foreign_key: :employee_id do
+        scope do |_employee_ids|
+          observed = {thread: Thread.current.object_id, user: RailsCurrentAttributesTest::Current.user}
+          Position.none
+        end
+      end
+
+      # A second sideload, since one on its own resolves inline and never reaches the pool.
+      resource_class.has_many :other_positions, resource: PositionResource, foreign_key: :employee_id do
+        scope { |_employee_ids| Position.none }
+      end
+
+      RailsCurrentAttributesTest::Current.user = "jeff"
+      resource_class.all(filter: {id: employee.id}, include: "probe_positions,other_positions").to_a
+
+      expect(observed[:thread]).to_not eq(Thread.current.object_id)
+      expect(observed[:user]).to eq("jeff")
+    end
+  end
+end
diff --git a/spec/integration/rails/cursor_pagination_spec.rb b/spec/integration/rails/cursor_pagination_spec.rb
index 25539481..f053481f 100644
--- a/spec/integration/rails/cursor_pagination_spec.rb
+++ b/spec/integration/rails/cursor_pagination_spec.rb
@@ -1,6 +1,6 @@
 if ENV["APPRAISAL_INITIALIZED"]
   RSpec.describe "cursor pagination", type: :controller do
-    include GraphitiSpecHelpers
+    include Graphiti::SpecHelpers
 
     controller(ApplicationController) do
       def index
@@ -28,12 +28,12 @@ def resource
     let!(:author4) { Legacy::Author.create!(age: 30, last_login: 1.days.ago) }
 
     around do |e|
-      original = Legacy::AuthorResource.cursor_paginatable
-      Legacy::AuthorResource.cursor_paginatable = true
+      original = Legacy::AuthorResource.page_cursors
+      Legacy::AuthorResource.page_cursors = true
       begin
         e.run
       ensure
-        Legacy::AuthorResource.cursor_paginatable = original
+        Legacy::AuthorResource.page_cursors = original
       end
     end
 
@@ -41,7 +41,7 @@ def decode(cursor)
       JSON.parse(Base64.decode64(cursor)).deep_symbolize_keys
     end
 
-    # don't go through 'd' helper b/c it is memoized
+    # don't go through jsonapi_data b/c it is memoized
     def ids
       json["data"].map { |d| d["id"].to_i }
     end
diff --git a/spec/integration/rails/debugging_spec.rb b/spec/integration/rails/debugging_spec.rb
new file mode 100644
index 00000000..1c6e6071
--- /dev/null
+++ b/spec/integration/rails/debugging_spec.rb
@@ -0,0 +1,27 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  RSpec.describe Graphiti::Rails::Debugging, type: :controller do
+    controller(ApplicationController) do
+      def index
+        render json: {}
+      end
+    end
+
+    it "comes along with Graphiti::Rails::Controller" do
+      expect(controller).to be_a(described_class)
+    end
+
+    it "wraps the action in a debugger" do
+      ran_inside_debugger = false
+      allow(Graphiti::Debugger).to receive(:debug).and_wrap_original do |original, &block|
+        original.call do
+          ran_inside_debugger = true
+          block.call
+        end
+      end
+
+      get :index
+
+      expect(ran_inside_debugger).to eq(true)
+    end
+  end
+end
diff --git a/spec/integration/rails/deprecated_rails_constants_spec.rb b/spec/integration/rails/deprecated_rails_constants_spec.rb
new file mode 100644
index 00000000..ed980c69
--- /dev/null
+++ b/spec/integration/rails/deprecated_rails_constants_spec.rb
@@ -0,0 +1,101 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  RSpec.describe "deprecated Rails constants" do
+    def silenced
+      Graphiti::DEPRECATOR.silence { yield }
+    end
+
+    describe "Graphiti::Responders" do
+      it "resolves to Graphiti::Rails::Responders" do
+        expect(silenced { Graphiti::Responders == Graphiti::Rails::Responders }).to eq(true)
+      end
+
+      it "warns when used" do
+        expect(Graphiti::DEPRECATOR).to receive(:warn).at_least(:once).and_return(nil)
+
+        Graphiti::Responders == Graphiti::Rails::Responders
+      end
+    end
+
+    describe "including Graphiti::Rails" do
+      it "warns" do
+        expect(Graphiti::DEPRECATOR).to receive(:deprecation_warning)
+          .with("Including Graphiti::Rails", a_string_including("Graphiti::Rails::Controller"))
+
+        Class.new(ActionController::Base) { include Graphiti::Rails }
+      end
+
+      it "still sets the controller up, rather than silently doing nothing" do
+        klass = silenced { Class.new(ActionController::Base) { include Graphiti::Rails } }
+
+        expect(klass.ancestors).to include(Graphiti::Rails::Controller)
+        expect(klass.ancestors).to include(Graphiti::Rails::Context)
+        expect(klass.rescue_registry.handles_exception?(Graphiti::Errors::RecordNotFound.new))
+          .to eq(true)
+      end
+    end
+
+    describe 'require "graphiti-rails"' do
+      it "still resolves" do
+        expect { silenced { require "graphiti-rails" } }.to_not raise_error
+      end
+    end
+
+    describe "Graphiti::Rails::DEPRECATOR" do
+      it "resolves on an explicit lookup, not just lexically" do
+        expect(Graphiti::Rails::DEPRECATOR).to eq(Graphiti::DEPRECATOR)
+      end
+    end
+
+    describe 'require "graphiti/responders"' do
+      it "still resolves" do
+        expect { silenced { require "graphiti/responders" } }.to_not raise_error
+      end
+    end
+
+    describe "Graphiti::Railtie" do
+      it "resolves to Graphiti::Rails::Railtie" do
+        expect(silenced { Graphiti::Railtie == Graphiti::Rails::Railtie }).to eq(true)
+      end
+    end
+
+    describe 'require "graphiti/railtie"' do
+      it "still resolves" do
+        expect { silenced { require "graphiti/railtie" } }.to_not raise_error
+      end
+    end
+
+    describe "Graphiti::Rails::GraphitiErrorsTesting" do
+      it "resolves to Graphiti::Rails::TestHelpers" do
+        expect(silenced { Graphiti::Rails::GraphitiErrorsTesting == Graphiti::Rails::TestHelpers })
+          .to eq(true)
+      end
+    end
+
+    describe "GraphitiErrors.disable!/enable!" do
+      around do |example|
+        original = ::Rails.application.config.action_dispatch.show_exceptions
+        example.run
+      ensure
+        ::Rails.application.config.action_dispatch.show_exceptions = original
+        ::Rails.application.env_config["action_dispatch.show_exceptions"] = original
+      end
+
+      it "warns" do
+        expect(Graphiti::DEPRECATOR).to receive(:deprecation_warning)
+          .with("GraphitiErrors.disable!", a_string_including("handle_request_exceptions"))
+
+        GraphitiErrors.disable!
+      end
+
+      it "still toggles exception rendering" do
+        silenced do
+          GraphitiErrors.enable!
+          expect(GraphitiErrors.disabled?).to eq(false)
+
+          GraphitiErrors.disable!
+          expect(GraphitiErrors.disabled?).to eq(true)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/integration/rails/exception_handling_spec.rb b/spec/integration/rails/exception_handling_spec.rb
new file mode 100644
index 00000000..c46c0853
--- /dev/null
+++ b/spec/integration/rails/exception_handling_spec.rb
@@ -0,0 +1,207 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  # rescue_registry renders exceptions from ActionDispatch middleware, which
+  # controller specs bypass entirely, so these have to be request specs.
+  RSpec.describe "exception handling", type: :request do
+    include Graphiti::Rails::TestHelpers
+
+    class SpecErrorsController < ApplicationController
+      def index
+        raise SPEC_ERROR
+      end
+    end
+
+    let(:invalid_request_errors) do
+      Graphiti::Util::SimpleErrors.new(Object.new).tap do |errors|
+        errors.add(:"filter.title", :unsupported, message: "is not supported")
+      end
+    end
+
+    before do
+      stub_const("SPEC_ERROR", error)
+      Rails.application.routes.draw do
+        get "/spec_errors" => "spec_errors#index"
+      end
+    end
+
+    # Exceptions propagate untouched in tests unless the request is wrapped.
+    def get_errors(accept: "application/vnd.api+json")
+      handle_request_exceptions do
+        get "/spec_errors", headers: {"HTTP_ACCEPT" => accept}
+      end
+    end
+
+    let(:json) { JSON.parse(response.body) }
+
+    context "Graphiti::Errors::InvalidRequest" do
+      let(:error) { Graphiti::Errors::InvalidRequest.new(invalid_request_errors) }
+
+      it "renders a 400 naming the rejected parameter" do
+        get_errors
+
+        expect(response.status).to eq(400)
+        expect(json["errors"]).to match([a_hash_including(
+          "code" => "bad_request",
+          "status" => "400",
+          "title" => "Request Error",
+          "source" => {"pointer" => "filter/title"},
+          "meta" => a_hash_including(
+            "attribute" => "filter.title",
+            "message" => "is not supported",
+            "code" => "unsupported"
+          )
+        )])
+      end
+    end
+
+    context "Graphiti::Errors::ConflictRequest" do
+      let(:error) { Graphiti::Errors::ConflictRequest.new(invalid_request_errors) }
+
+      it "renders a 409, not the 400 its superclass would give" do
+        get_errors
+
+        expect(response.status).to eq(409)
+        expect(json["errors"][0]).to include(
+          "code" => "conflict",
+          "status" => "409",
+          "title" => "Conflict Error"
+        )
+      end
+    end
+
+    context "Graphiti::Errors::RecordNotFound" do
+      let(:error) { Graphiti::Errors::RecordNotFound.new("employees", 123) }
+
+      it "renders a 404 with the exception message as detail" do
+        get_errors
+
+        expect(response.status).to eq(404)
+        expect(json["errors"][0]["code"]).to eq("not_found")
+        expect(json["errors"][0]["detail"])
+          .to eq("The referenced resource 'employees' with id '123' could not be found.")
+      end
+    end
+
+    context "a client-caused query error" do
+      let(:error) do
+        Graphiti::Errors::InvalidInclude.new(PORO::EmployeeResource.new, "foo")
+      end
+
+      it "renders a 400 with the exception message as detail" do
+        get_errors
+
+        expect(response.status).to eq(400)
+        expect(json["errors"][0]["detail"]).to match(/"foo" is not supported/)
+      end
+    end
+
+    context "paginating a sideload across multiple parents" do
+      let(:error) { Graphiti::Errors::UnsupportedPagination.new }
+
+      it "renders a 400, not the 500 an unregistered error would give" do
+        get_errors
+
+        expect(response.status).to eq(400)
+        expect(json["errors"][0]["detail"]).to match(/pagination of a sideload/)
+      end
+    end
+
+    context "an unregistered exception" do
+      let(:error) { RuntimeError.new("boom") }
+
+      it "is rendered as jsonapi by the fallback handler" do
+        get_errors
+
+        expect(response.status).to eq(500)
+        expect(response.content_type).to start_with("application/vnd.api+json")
+      end
+
+      it "claims nothing on the application's behalf" do
+        get_errors
+
+        expect(json["errors"][0]).to eq(
+          "code" => "internal_server_error",
+          "status" => "500",
+          "title" => "Internal Server Error"
+        )
+      end
+
+      context "when a locale names the error" do
+        around do |example|
+          original = ::I18n.backend
+          ::I18n.backend = ::I18n::Backend::Simple.new
+          ::I18n.backend.store_translations(:en, graphiti: {
+            errors: {internal_server_error: {title: "Omethingsay entway ongwray", detail: "Ytray againway ortlyshay."}}
+          })
+          example.run
+        ensure
+          ::I18n.backend = original
+        end
+
+        it "renders the title and detail from it" do
+          get_errors
+
+          expect(json["errors"][0]).to include(
+            "title" => "Omethingsay entway ongwray",
+            "detail" => "Ytray againway ortlyshay."
+          )
+        end
+      end
+
+      # The override dropped rescue_registry's status_code >= 500 guard, so the
+      # lookup runs at every status the fallback passes through to.
+      context "when the exception passes through to a status of its own" do
+        let(:error) { ActionController::RoutingError.new("nope") }
+
+        around do |example|
+          original = ::I18n.backend
+          ::I18n.backend = ::I18n::Backend::Simple.new
+          ::I18n.backend.store_translations(:en, graphiti: {
+            errors: {not_found: {detail: "Onegay orevermoreflay."}}
+          })
+          example.run
+        ensure
+          ::I18n.backend = original
+        end
+
+        it "reads the detail keyed by that status, not the fallback's" do
+          get_errors
+
+          expect(response.status).to eq(404)
+          expect(json["errors"][0]).to include(
+            "code" => "not_found",
+            "detail" => "Onegay orevermoreflay."
+          )
+        end
+      end
+
+      # FallbackHandler returns nil for anything outside
+      # handled_exception_formats, handing the request back to Rails.
+      it "is left to Rails for formats Graphiti does not handle" do
+        get_errors(accept: "text/html")
+
+        expect(response.status).to eq(500)
+        expect(response.content_type).to_not include("vnd.api+json")
+      end
+    end
+
+    context "when request exception handling is turned off" do
+      let(:error) { Graphiti::Errors::RecordNotFound.new }
+
+      it "lets the exception through so specs can assert on it directly" do
+        handle_request_exceptions(false) do
+          expect {
+            get "/spec_errors", headers: {"HTTP_ACCEPT" => "application/vnd.api+json"}
+          }.to raise_error(Graphiti::Errors::RecordNotFound)
+        end
+      end
+
+      it "restores the previous setting afterwards" do
+        handle_request_exceptions(false) {}
+
+        get_errors
+
+        expect(response.status).to eq(404)
+      end
+    end
+  end
+end
diff --git a/spec/integration/rails/finders_spec.rb b/spec/integration/rails/finders_spec.rb
index ca7ff869..bd519691 100644
--- a/spec/integration/rails/finders_spec.rb
+++ b/spec/integration/rails/finders_spec.rb
@@ -1,6 +1,6 @@
 if ENV["APPRAISAL_INITIALIZED"]
   RSpec.describe "integrated resources and adapters", type: :controller do
-    include GraphitiSpecHelpers
+    include Graphiti::SpecHelpers
 
     controller(ApplicationController) do
       def index
@@ -74,22 +74,22 @@ def resource
 
     it "allows basic sorting" do
       do_index({sort: "-id"})
-      expect(d.map(&:id)).to eq([author2.id, author1.id])
+      expect(jsonapi_data.map(&:id)).to eq([author2.id, author1.id])
     end
 
     it "allows allowlisted filters (and other configs)" do
       do_index({filter: {first_name: "George"}})
-      expect(d.map(&:id)).to eq([author2.id])
+      expect(jsonapi_data.map(&:id)).to eq([author2.id])
     end
 
     it "allows basic sideloading" do
       do_index({include: "books"})
-      expect(included.map(&:jsonapi_type).uniq).to match_array(%w[books])
+      expect(jsonapi_included.map(&:jsonapi_type).uniq).to match_array(%w[books])
     end
 
     it "allows nested sideloading" do
       do_index({include: "books.genre"})
-      expect(included.map(&:jsonapi_type).uniq)
+      expect(jsonapi_included.map(&:jsonapi_type).uniq)
         .to match_array(%w[books genres])
     end
 
@@ -99,7 +99,7 @@ def resource
 
       subject do
         do_index(page: params)
-        d.map(&:id)
+        jsonapi_data.map(&:id)
       end
 
       context "by number and size" do
@@ -130,13 +130,10 @@ def resource
         let(:params) { {size: 1, number: 2} }
 
         around do |e|
-          original = Graphiti.config.pagination_links
-          begin
-            Graphiti.config.pagination_links = true
-            e.run
-          ensure
-            Graphiti.config.pagination_links = original
-          end
+          Legacy::AuthorResource.page_links = true
+          e.run
+        ensure
+          Legacy::AuthorResource.page_links = false
         end
 
         let(:links) do
@@ -174,7 +171,7 @@ def resource
             it "works" do
               do_index(page: params)
 
-              expect(d[0].id).to eq(author3.id)
+              expect(jsonapi_data[0].id).to eq(author3.id)
               expect(links["self"])
                 .to eq("page[number]=2&page[offset]=1&page[size]=1")
               expect(links["first"])
@@ -194,7 +191,7 @@ def resource
 
               it "does not render 'next'" do
                 do_index(page: params)
-                expect(d[0].id).to eq(author4.id)
+                expect(jsonapi_data[0].id).to eq(author4.id)
                 expect(links).to_not have_key("next")
               end
             end
@@ -206,7 +203,7 @@ def resource
     describe "filtering" do
       subject(:ids) do
         do_index({filter: filter})
-        d.map(&:id)
+        jsonapi_data.map(&:id)
       end
 
       let!(:author3) do
@@ -253,24 +250,24 @@ def resource
         context "eq" do
           it "executes case-sensitive search (invalid)" do
             do_index({filter: {identifier: "abc123"}})
-            expect(d.map(&:id)).to eq([])
+            expect(jsonapi_data.map(&:id)).to eq([])
           end
 
           it "executes case-sensitive search (valid)" do
             do_index({filter: {identifier: "AbC123"}})
-            expect(d.map(&:id)).to eq([author3.id])
+            expect(jsonapi_data.map(&:id)).to eq([author3.id])
           end
         end
 
         context "!eq" do
           it "executes case-sensitive search (invalid)" do
             do_index({filter: {identifier: {not_eq: "abc123"}}})
-            expect(d.map(&:id).length).to eq(3)
+            expect(jsonapi_data.map(&:id).length).to eq(3)
           end
 
           it "executes case-sensitive search (valid)" do
             do_index({filter: {identifier: {not_eq: "AbC123"}}})
-            expect(d.map(&:id).length).to eq(2)
+            expect(jsonapi_data.map(&:id).length).to eq(2)
           end
         end
       end
@@ -321,14 +318,14 @@ def resource
         end
 
         context "!eq" do
-          let(:value) { {'!eq': "george"} }
+          let(:value) { {"!eq": "george"} }
 
           it "executes case-insensitive NOT search" do
             expect(ids).to eq([author1.id])
           end
 
           context "with nil value" do
-            let(:value) { {'!eq': nil} }
+            let(:value) { {"!eq": nil} }
             let!(:author3) { Legacy::Author.create! }
 
             it "works" do
@@ -365,7 +362,7 @@ def resource
         end
 
         context "!eql" do
-          let(:value) { {'!eql': "GeOrge"} }
+          let(:value) { {"!eql": "GeOrge"} }
 
           it "executes case-sensitive search" do
             expect(ids).to eq([author1.id, author2.id])
@@ -399,7 +396,7 @@ def resource
         end
 
         context "!prefix" do
-          let(:value) { {'!prefix': "Geo"} }
+          let(:value) { {"!prefix": "Geo"} }
 
           it "executes case-insensitive prefix NOT query" do
             expect(ids).to eq([author1.id])
@@ -433,7 +430,7 @@ def resource
         end
 
         context "!suffix" do
-          let(:value) { {'!suffix': "orge"} }
+          let(:value) { {"!suffix": "orge"} }
 
           it "executes case-insensitive suffix NOT query" do
             expect(ids).to eq([author1.id])
@@ -467,7 +464,7 @@ def resource
         end
 
         context "!match" do
-          let(:value) { {'!match': "org"} }
+          let(:value) { {"!match": "org"} }
 
           it "executes case-insensitive NOT match query" do
             expect(ids).to eq([author1.id])
@@ -487,7 +484,7 @@ def resource
         end
 
         context "!eq" do
-          let(:value) { {'!eq': 65} }
+          let(:value) { {"!eq": 65} }
 
           it "works" do
             expect(ids).to eq([author1.id, author3.id])
@@ -539,14 +536,14 @@ def resource
         let(:filter) { {decimal_age: value} }
 
         context "eq" do
-          let(:value) { {eq: 70.011.to_d} }
+          let(:value) { {eq: BigDecimal("70.011")} }
 
           it "works" do
             expect(ids).to eq([author2.id])
           end
 
           context "as a string" do
-            let(:value) { {eq: 70.011.to_d.to_s} }
+            let(:value) { {eq: BigDecimal("70.011").to_s} }
 
             it "works" do
               expect(ids).to eq([author2.id])
@@ -555,7 +552,7 @@ def resource
         end
 
         context "!eq" do
-          let(:value) { {'!eq': 70.011.to_d} }
+          let(:value) { {"!eq": BigDecimal("70.011")} }
 
           it "works" do
             expect(ids).to eq([author1.id, author3.id])
@@ -563,7 +560,7 @@ def resource
         end
 
         context "nothing" do
-          let(:value) { 70.011.to_d }
+          let(:value) { BigDecimal("70.011") }
 
           it "defaults to eq" do
             expect(ids).to eq([author2.id])
@@ -571,7 +568,7 @@ def resource
         end
 
         context "gt" do
-          let(:value) { {gt: 70.033.to_d} }
+          let(:value) { {gt: BigDecimal("70.033")} }
 
           it "works" do
             expect(ids).to eq([author3.id])
@@ -579,7 +576,7 @@ def resource
         end
 
         context "gte" do
-          let(:value) { {gte: 70.033.to_d} }
+          let(:value) { {gte: BigDecimal("70.033")} }
 
           it "works" do
             expect(ids).to eq([author1.id, author3.id])
@@ -587,7 +584,7 @@ def resource
         end
 
         context "lt" do
-          let(:value) { {lt: 70.033.to_d} }
+          let(:value) { {lt: BigDecimal("70.033")} }
 
           it "works" do
             expect(ids).to eq([author2.id])
@@ -595,7 +592,7 @@ def resource
         end
 
         context "lte" do
-          let(:value) { {lte: 70.033.to_d} }
+          let(:value) { {lte: BigDecimal("70.033")} }
 
           it "works" do
             expect(ids).to eq([author1.id, author2.id])
@@ -615,7 +612,7 @@ def resource
         end
 
         context "!eq" do
-          let(:value) { {'!eq': 70.01} }
+          let(:value) { {"!eq": 70.01} }
 
           it "works" do
             expect(ids).to eq([author1.id, author3.id])
@@ -675,7 +672,7 @@ def resource
         end
 
         context "!eq" do
-          let(:value) { {'!eq': two_days_ago.to_date.iso8601} }
+          let(:value) { {"!eq": two_days_ago.to_date.iso8601} }
 
           it "works" do
             expect(ids).to eq([author1.id, author3.id])
@@ -744,7 +741,7 @@ def resource
         end
 
         context "!eq" do
-          let(:value) { {'!eq': two_days_ago.iso8601} }
+          let(:value) { {"!eq": two_days_ago.iso8601} }
 
           it "works" do
             expect(ids).to eq([author1.id, author3.id])
@@ -752,7 +749,7 @@ def resource
 
           context "when value is nil" do
             let(:filter) { {last_login: value} }
-            let(:value) { {'!eq': "null"} }
+            let(:value) { {"!eq": "null"} }
 
             it "works" do
               expect(ids).to eq([author1.id, author2.id])
@@ -1035,7 +1032,7 @@ def resource
           "decimal_age" => "70.033",
           "active" => true
         })
-        expect(included.map(&:jsonapi_type).uniq).to match_array(%w[books])
+        expect(jsonapi_included.map(&:jsonapi_type).uniq).to match_array(%w[books])
       end
 
       context "and record not found" do
@@ -1057,7 +1054,7 @@ def resource
             include: "books"
           })
           expect(json["data"][0]["relationships"]).to be_present
-          expect(included.map(&:jsonapi_type).uniq).to match_array(%w[books])
+          expect(jsonapi_included.map(&:jsonapi_type).uniq).to match_array(%w[books])
         end
       end
     end
@@ -1065,7 +1062,7 @@ def resource
     context "sideloading has_many" do
       it "can sideload" do
         do_index({include: "books"})
-        expect(included("books").map(&:id)).to eq([book1.id, book2.id])
+        expect(jsonapi_included("books").map(&:id)).to eq([book1.id, book2.id])
       end
 
       context "when paginating the sideload" do
@@ -1083,7 +1080,7 @@ def resource
 
           it "works" do
             request
-            expect(included("books").map(&:id)).to eq([book2.id])
+            expect(jsonapi_included("books").map(&:id)).to eq([book2.id])
           end
         end
 
@@ -1102,17 +1099,17 @@ def resource
 
       it "allows sorting of sideloaded resource" do
         do_index({include: "books", sort: "-books.id"})
-        expect(included("books").map(&:id)).to eq([book2.id, book1.id])
+        expect(jsonapi_included("books").map(&:id)).to eq([book2.id, book1.id])
       end
 
       it "allows filtering of sideloaded resource" do
         do_index({include: "books", filter: {books: {id: book2.id}}})
-        expect(included("books").map(&:id)).to eq([book2.id])
+        expect(jsonapi_included("books").map(&:id)).to eq([book2.id])
       end
 
       it "allows extra fields for sideloaded resource" do
         do_index({include: "books", extra_fields: {books: "alternate_title"}})
-        book = included("books")[0]
+        book = jsonapi_included("books")[0]
         expect(book["title"]).to be_present
         expect(book["pages"]).to be_present
         expect(book["alternate_title"]).to eq("alt title")
@@ -1120,7 +1117,7 @@ def resource
 
       it "allows sparse fieldsets for the sideloaded resource" do
         do_index({include: "books", fields: {books: "pages"}})
-        book = included("books")[0]
+        book = jsonapi_included("books")[0]
         expect(book).to_not have_key("title")
         expect(book).to_not have_key("alternate_title")
         expect(book["pages"]).to eq(500)
@@ -1128,7 +1125,7 @@ def resource
 
       it "allows extra fields and sparse fieldsets for the sideloaded resource" do
         do_index({include: "books", fields: {books: "pages"}, extra_fields: {books: "alternate_title"}})
-        book = included("books")[0]
+        book = jsonapi_included("books")[0]
         expect(book).to have_key("pages")
         expect(book).to have_key("alternate_title")
         expect(book).to_not have_key("title")
@@ -1149,7 +1146,7 @@ def resource
 
         it "still works" do
           do_index({include: "books"})
-          expect(included("books").map(&:id)).to eq([book1.id, book2.id])
+          expect(jsonapi_included("books").map(&:id)).to eq([book1.id, book2.id])
         end
       end
     end
@@ -1157,12 +1154,12 @@ def resource
     context "sideloading belongs_to" do
       it "can sideload" do
         do_index({include: "state"})
-        expect(included("states").map(&:id)).to eq([state.id])
+        expect(jsonapi_included("states").map(&:id)).to eq([state.id])
       end
 
       it "allows extra fields for sideloaded resource" do
         do_index({include: "state", extra_fields: {states: "population"}})
-        state = included("states")[0]
+        state = jsonapi_included("states")[0]
         expect(state["name"]).to be_present
         expect(state["abbreviation"]).to be_present
         expect(state["population"]).to be_present
@@ -1170,7 +1167,7 @@ def resource
 
       it "allows sparse fieldsets for the sideloaded resource" do
         do_index({include: "state", fields: {states: "name"}})
-        state = included("states")[0]
+        state = jsonapi_included("states")[0]
         expect(state["name"]).to be_present
         expect(state).to_not have_key("abbreviation")
         expect(state).to_not have_key("population")
@@ -1178,7 +1175,7 @@ def resource
 
       it "allows extra fields and sparse fieldsets for the sideloaded resource" do
         do_index({include: "state", fields: {states: "name"}, extra_fields: {states: "population"}})
-        state = included("states")[0]
+        state = jsonapi_included("states")[0]
         expect(state).to have_key("name")
         expect(state).to have_key("population")
         expect(state).to_not have_key("abbreviation")
@@ -1188,12 +1185,12 @@ def resource
     context "sideloading has_one" do
       it "can sideload" do
         do_index({include: "bio"})
-        expect(included("bios").map(&:id)).to eq([bio.id])
+        expect(jsonapi_included("bios").map(&:id)).to eq([bio.id])
       end
 
       it "allows extra fields for sideloaded resource" do
         do_index({include: "bio", extra_fields: {bios: "created_at"}})
-        bio = included("bios")[0]
+        bio = jsonapi_included("bios")[0]
         expect(bio["description"]).to be_present
         expect(bio["created_at"]).to be_present
         expect(bio["picture"]).to be_present
@@ -1201,7 +1198,7 @@ def resource
 
       it "allows sparse fieldsets for the sideloaded resource" do
         do_index({include: "bio", fields: {bios: "description"}})
-        bio = included("bios")[0]
+        bio = jsonapi_included("bios")[0]
         expect(bio["description"]).to be_present
         expect(bio).to_not have_key("created_at")
         expect(bio).to_not have_key("picture")
@@ -1209,7 +1206,7 @@ def resource
 
       it "allows extra fields and sparse fieldsets for the sideloaded resource" do
         do_index({include: "bio", fields: {bios: "description"}, extra_fields: {bios: "created_at"}})
-        bio = included("bios")[0]
+        bio = jsonapi_included("bios")[0]
         expect(bio).to have_key("description")
         expect(bio).to have_key("created_at")
         expect(bio).to_not have_key("picture")
@@ -1240,7 +1237,7 @@ def resource
 
           it "still works" do
             do_index({include: "bio.bio_labels"})
-            expect(included("bio_labels").length).to eq(1)
+            expect(jsonapi_included("bio_labels").length).to eq(1)
           end
         end
       end
@@ -1249,12 +1246,12 @@ def resource
     context "sideloading many_to_many" do
       it "can sideload" do
         do_index({include: "hobbies"})
-        expect(included("hobbies").map(&:id)).to eq([hobby1.id, hobby2.id])
+        expect(jsonapi_included("hobbies").map(&:id)).to eq([hobby1.id, hobby2.id])
       end
 
       it "allows sorting of sideloaded resource" do
         do_index({include: "hobbies", sort: "-hobbies.name"})
-        expect(included("hobbies").map(&:id)).to eq([hobby2.id, hobby1.id])
+        expect(jsonapi_included("hobbies").map(&:id)).to eq([hobby2.id, hobby1.id])
       end
 
       it "allows filtering of sideloaded resource" do
@@ -1262,7 +1259,7 @@ def resource
           include: "hobbies",
           filter: {hobbies: {id: hobby2.id}}
         })
-        expect(included("hobbies").map(&:id)).to eq([hobby2.id])
+        expect(jsonapi_included("hobbies").map(&:id)).to eq([hobby2.id])
       end
 
       it "allows extra fields for sideloaded resource" do
@@ -1270,7 +1267,7 @@ def resource
           include: "hobbies",
           extra_fields: {hobbies: "reason"}
         })
-        hobby = included("hobbies")[0]
+        hobby = jsonapi_included("hobbies")[0]
         expect(hobby["name"]).to be_present
         expect(hobby["description"]).to be_present
         expect(hobby["reason"]).to eq("hobby reason")
@@ -1278,7 +1275,7 @@ def resource
 
       it "allows sparse fieldsets for the sideloaded resource" do
         do_index({include: "hobbies", fields: {hobbies: "name"}})
-        hobby = included("hobbies")[0]
+        hobby = jsonapi_included("hobbies")[0]
         expect(hobby["name"]).to be_present
         expect(hobby).to_not have_key("description")
         expect(hobby).to_not have_key("reason")
@@ -1290,7 +1287,7 @@ def resource
           fields: {hobbies: "name"},
           extra_fields: {hobbies: "reason"}
         })
-        hobby = included("hobbies")[0]
+        hobby = jsonapi_included("hobbies")[0]
         expect(hobby).to have_key("name")
         expect(hobby).to have_key("reason")
         expect(hobby).to_not have_key("description")
@@ -1302,8 +1299,8 @@ def resource
           fields: {hobbies: "name", books: "title"},
           extra_fields: {hobbies: "reason", books: "alternate_title"}
         })
-        hobby = included("hobbies")[0]
-        book = included("books")[0]
+        hobby = jsonapi_included("hobbies")[0]
+        book = jsonapi_included("books")[0]
         expect(hobby).to have_key("name")
         expect(hobby).to have_key("reason")
         expect(hobby).to_not have_key("description")
@@ -1320,7 +1317,7 @@ def resource
         author1_hobbies = author1_relationships["hobbies"]["data"]
         author2_hobbies = author2_relationships["hobbies"]["data"]
 
-        expect(included("hobbies").size).to eq(2)
+        expect(jsonapi_included("hobbies").size).to eq(2)
         expect(author1_hobbies.size).to eq(2)
         expect(author2_hobbies.size).to eq(1)
       end
@@ -1329,14 +1326,14 @@ def resource
         let(:shop) { Legacy::Sales::Shop.create(name: "shop") }
 
         before do
-          allow(Legacy::Sales::ShopResource).to receive(:validate_endpoints?) { false }
+          allow(Legacy::Sales::ShopResource).to receive(:validate_requests?) { false }
           allow(controller).to receive(:resource).and_return(Legacy::Sales::ShopResource)
           Legacy::Sales::Stock.create(shop_id: shop.id, book_id: book1.id, amount: 2)
         end
 
         it "works" do
           do_index({include: "books"})
-          expect(included("books").map(&:id)).to eq([book1.id])
+          expect(jsonapi_included("books").map(&:id)).to eq([book1.id])
         end
       end
 
@@ -1392,7 +1389,7 @@ def resource
 
         it "still works" do
           do_index({include: "mentors"})
-          target = d.find { |e| e.id == author_with_mentors.id }
+          target = jsonapi_data.find { |e| e.id == author_with_mentors.id }
           expect(target.relationships["mentors"]).to eq({
             "data" => [
               {"type" => "authors", "id" => author_with_mentees.id.to_s},
@@ -1404,7 +1401,7 @@ def resource
         it "allows filtering by the association" do
           do_index({filter: {mentor_id: author_with_mentees.id}})
 
-          expect(d.map(&:id)).to eq([author_with_mentors.id, author_with_both.id])
+          expect(jsonapi_data.map(&:id)).to eq([author_with_mentors.id, author_with_both.id])
         end
       end
 
@@ -1413,7 +1410,7 @@ def resource
           Legacy::UserResource.class_eval do
             many_to_many :books, resource: Legacy::BookResource
           end
-          allow(Legacy::BookResource).to receive(:validate_endpoints?) { false }
+          allow(Legacy::BookResource).to receive(:validate_requests?) { false }
           allow(controller).to receive(:resource) { Legacy::BookResource }
         end
 
@@ -1426,7 +1423,7 @@ def resource
         it "correctly infers the filter name for the association from the inverse_of option" do
           do_index({filter: {reader_id: reader.id}})
 
-          expect(d.map(&:id)).to eq([book2.id])
+          expect(jsonapi_data.map(&:id)).to eq([book2.id])
         end
 
         context "when the graphiti association manually sets inverse_filter" do
@@ -1439,7 +1436,7 @@ def resource
           it "overrides the inferred one" do
             do_index({filter: {the_reader_id: reader.id}})
 
-            expect(d.map(&:id)).to eq([book2.id])
+            expect(jsonapi_data.map(&:id)).to eq([book2.id])
           end
         end
       end
@@ -1466,7 +1463,7 @@ def resource
 
         it "still works" do
           do_index({include: "hobbies"})
-          expect(included("hobbies").map(&:id))
+          expect(jsonapi_included("hobbies").map(&:id))
             .to eq([other_table_hobby1.id, other_table_hobby2.id])
         end
       end
@@ -1484,8 +1481,7 @@ def resource
             required: false,
             operators:
               {eq: nil, not_eq: nil, gt: nil, gte: nil, lt: nil, lte: nil},
-            allow_nil: false,
-            deny_empty: false
+            blanks: :literal
           }
         }
 
@@ -1533,7 +1529,7 @@ def resource
 
         it "still works" do
           do_index({include: "hobbies"})
-          expect(included("hobbies").map(&:id)).to eq([hobby1.id, hobby2.id])
+          expect(jsonapi_included("hobbies").map(&:id)).to eq([hobby1.id, hobby2.id])
         end
 
         describe "filtering relationship" do
@@ -1551,7 +1547,7 @@ def index
 
           it "can filter the relationship by the custom name" do
             do_index(filter: {the_id_of_the_author: [author1.id, author2.id].join(",")})
-            expect(d.map(&:id)).to eq([hobby1.id, hobby2.id])
+            expect(jsonapi_data.map(&:id)).to eq([hobby1.id, hobby2.id])
           end
         end
       end
@@ -1569,9 +1565,9 @@ def index
 
           it "still works" do
             do_index({include: "tags"})
-            sl = d[0].sideload(:tags)
+            sl = jsonapi_data[0].sideload(:tags)
             expect(sl.map(&:id)).to eq([tag1.id])
-            sl = d[1].sideload(:tags)
+            sl = jsonapi_data[1].sideload(:tags)
             expect(sl.map(&:id)).to eq([tag1.id, tag2.id])
           end
         end
@@ -1584,12 +1580,12 @@ def index
           end
 
           it "still works" do
-            allow(Legacy::BookResource).to receive(:validate_endpoints?) { false }
+            allow(Legacy::BookResource).to receive(:validate_requests?) { false }
             allow(controller).to receive(:resource) { Legacy::BookResource }
             do_index({include: "tags"})
-            sl = d[0].sideload(:tags)
+            sl = jsonapi_data[0].sideload(:tags)
             expect(sl.map(&:id)).to eq([tag1.id, tag2.id])
-            sl = d[1].sideload(:tags)
+            sl = jsonapi_data[1].sideload(:tags)
             expect(sl.map(&:id)).to eq([tag1.id])
           end
         end
@@ -1605,7 +1601,7 @@ def index
             end
 
             it "still works" do
-              allow(Legacy::TagResource).to receive(:validate_endpoints?) { false }
+              allow(Legacy::TagResource).to receive(:validate_requests?) { false }
               allow(controller).to receive(:resource) { Legacy::TagResource }
               do_index({
                 filter: {
@@ -1615,7 +1611,7 @@ def index
                   ]
                 }
               })
-              expect(d.map(&:name)).to eq(%w[One Two Three])
+              expect(jsonapi_data.map(&:name)).to eq(%w[One Two Three])
             end
           end
         end
@@ -1625,7 +1621,7 @@ def index
     context "sideloading self-referential" do
       it "works" do
         do_index({include: "organization.children"})
-        includes = included("organizations")
+        includes = jsonapi_included("organizations")
         expect(includes[0]["name"]).to eq("Org1")
         expect(includes[1]["name"]).to eq("Org2")
       end
@@ -1651,7 +1647,7 @@ def index
           filter: {books: {id: book1.id}, other_books: {id: book2.id}},
           include: "books.genre,other_books.genre"
         })
-        expect(included("genres").length).to eq(2)
+        expect(jsonapi_included("genres").length).to eq(2)
       end
     end
 
@@ -1661,11 +1657,11 @@ def index
           include: "dwelling",
           extra_fields: {houses: "house_price", condos: "condo_price"}
         })
-        house = included("houses")[0]
+        house = jsonapi_included("houses")[0]
         expect(house["name"]).to be_present
         expect(house["house_description"]).to be_present
         expect(house["house_price"]).to eq(1_000_000)
-        condo = included("condos")[0]
+        condo = jsonapi_included("condos")[0]
         expect(condo["name"]).to be_present
         expect(condo["condo_description"]).to be_present
         expect(condo["condo_price"]).to eq(500_000)
@@ -1676,11 +1672,11 @@ def index
           include: "dwelling",
           fields: {houses: "name", condos: "condo_description"}
         })
-        house = included("houses")[0]
+        house = jsonapi_included("houses")[0]
         expect(house["name"]).to be_present
         expect(house).to_not have_key("house_description")
         expect(house).to_not have_key("house_price")
-        condo = included("condos")[0]
+        condo = jsonapi_included("condos")[0]
         expect(condo["condo_description"]).to be_present
         expect(condo).to_not have_key("name")
         expect(condo).to_not have_key("condo_price")
@@ -1692,8 +1688,8 @@ def index
           fields: {houses: "name", condos: "condo_description"},
           extra_fields: {houses: "house_price", condos: "condo_price"}
         })
-        house = included("houses")[0]
-        condo = included("condos")[0]
+        house = jsonapi_included("houses")[0]
+        condo = jsonapi_included("condos")[0]
         expect(house).to have_key("name")
         expect(house).to have_key("house_price")
         expect(house).to_not have_key("house_description")
@@ -1705,7 +1701,7 @@ def index
       # NB: Condo does NOT have a state relationship
       it "allows additional levels of nesting" do
         do_index({include: "dwelling.state"})
-        expect(included("states").length).to eq(1)
+        expect(jsonapi_included("states").length).to eq(1)
       end
     end
 
@@ -1720,7 +1716,7 @@ def base_scope
 
       it "can query stats total count" do
         do_index({stats: {total: "count"}})
-        expect(d.map(&:id)).to eq([author1.id, author2.id])
+        expect(jsonapi_data.map(&:id)).to eq([author1.id, author2.id])
       end
     end
   end
diff --git a/spec/integration/rails/hooks_spec.rb b/spec/integration/rails/hooks_spec.rb
index a136a35f..a32789ea 100644
--- a/spec/integration/rails/hooks_spec.rb
+++ b/spec/integration/rails/hooks_spec.rb
@@ -137,8 +137,8 @@ def json
 
     context "after_save" do
       before do
-        book_data << {'temp-id': "abc123", type: "books", method: "create"}
-        book_included << {'temp-id': "abc123", type: "books", attributes: {title: "one"}}
+        book_data << {"temp-id": "abc123", type: "books", method: "create"}
+        book_included << {"temp-id": "abc123", type: "books", attributes: {title: "one"}}
         book_data << {id: update_book.id.to_s, type: "books", method: "update"}
         book_included << {id: update_book.id.to_s, type: "books", attributes: {title: "updated!"}}
       end
@@ -146,10 +146,10 @@ def json
 
     context "after_create" do
       before do
-        book_data << {'temp-id': "abc123", type: "books", method: "create"}
-        book_included << {'temp-id': "abc123", type: "books", attributes: {title: "one"}}
-        book_data << {'temp-id': "abc456", type: "books", method: "create"}
-        book_included << {'temp-id': "abc456", type: "books", attributes: {title: "two"}}
+        book_data << {"temp-id": "abc123", type: "books", method: "create"}
+        book_included << {"temp-id": "abc123", type: "books", attributes: {title: "one"}}
+        book_data << {"temp-id": "abc456", type: "books", method: "create"}
+        book_included << {"temp-id": "abc456", type: "books", attributes: {title: "two"}}
       end
 
       it "fires hooks correctly" do
@@ -230,10 +230,10 @@ def json
     end
 
     context "belongs_to" do
-      let(:state_data) { {'temp-id': "abc123", type: "states", method: "create"} }
+      let(:state_data) { {"temp-id": "abc123", type: "states", method: "create"} }
 
       before do
-        state_included << {'temp-id': "abc123", type: "states", attributes: {name: "New York"}}
+        state_included << {"temp-id": "abc123", type: "states", attributes: {name: "New York"}}
       end
 
       it "also works" do
diff --git a/spec/integration/rails/i18n_spec.rb b/spec/integration/rails/i18n_spec.rb
index 98593023..73624a11 100644
--- a/spec/integration/rails/i18n_spec.rb
+++ b/spec/integration/rails/i18n_spec.rb
@@ -1,7 +1,9 @@
 if ENV["APPRAISAL_INITIALIZED"]
   RSpec.describe "i18n features" do
+    # An earlier lookup leaves the backend initialized, and it reads load_path once.
     before(:all) do
       I18n.load_path << File.expand_path("../../support/locale/documentation_i18n.yml", __dir__)
+      I18n.reload!
     end
 
     describe "Type descriptions" do
diff --git a/spec/integration/rails/install_generator_spec.rb b/spec/integration/rails/install_generator_spec.rb
new file mode 100644
index 00000000..edf37938
--- /dev/null
+++ b/spec/integration/rails/install_generator_spec.rb
@@ -0,0 +1,94 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  require "rails/generators"
+  require "generators/graphiti/install_generator"
+  require "fileutils"
+  require "tmpdir"
+
+  RSpec.describe Graphiti::InstallGenerator do
+    let(:destination) { Dir.mktmpdir("graphiti-install") }
+
+    def generated(path)
+      File.read(File.join(destination, path))
+    end
+
+    def install!(*arguments)
+      Dir.chdir(destination) do
+        described_class.start(
+          arguments,
+          destination_root: destination, shell: Thor::Shell::Basic.new
+        )
+      end
+    end
+
+    before do
+      allow(::Rails).to receive(:root).and_return(Pathname.new(destination))
+
+      FileUtils.mkdir_p(File.join(destination, "app/controllers"))
+      FileUtils.mkdir_p(File.join(destination, "config"))
+      File.write(File.join(destination, "app/controllers/application_controller.rb"), <<~RUBY)
+        class ApplicationController < ActionController::API
+        end
+      RUBY
+      File.write(File.join(destination, "config/application.rb"), <<~RUBY)
+        module Dummy
+          class Application < Rails::Application
+          end
+        end
+      RUBY
+      File.write(File.join(destination, "config/routes.rb"), <<~RUBY)
+        Rails.application.routes.draw do
+        end
+      RUBY
+      # Present so the generator does not prompt for an API namespace on stdin.
+      File.write(File.join(destination, ".graphiticfg.yml"), {"namespace" => "/api/v1"}.to_yaml)
+    end
+
+    after { FileUtils.remove_entry(destination) }
+
+    it "still finishes when the app has no rails_helper to inject into" do
+      install!
+
+      expect(generated("config/routes.rb")).to include("scope path:")
+    end
+
+    it "wires the schema check into an existing rails_helper" do
+      FileUtils.mkdir_p(File.join(destination, "spec"))
+      File.write(File.join(destination, "spec/rails_helper.rb"), <<~RUBY)
+        RSpec.configure do |config|
+        end
+      RUBY
+
+      install!
+
+      expect(generated("spec/rails_helper.rb")).to include("Graphiti::SpecHelpers::RSpec.schema!")
+      expect(generated("config/routes.rb")).to include("scope path:")
+    end
+
+    it "writes the locale file too" do
+      install!
+
+      expect(File.exist?(File.join(destination, "config/locales/graphiti.en.yml"))).to eq(true)
+    end
+
+    it "leaves config/application.rb alone" do
+      install!
+
+      expect(generated("config/application.rb")).to_not include("default_url_options")
+    end
+
+    it "scopes routes under the namespace without naming ApplicationResource" do
+      install!
+
+      expect(generated("config/routes.rb"))
+        .to include(%(scope path: "/api/v1", defaults: {format: :jsonapi} do))
+      expect(generated("config/routes.rb")).to_not include("ApplicationResource")
+    end
+
+    it "gives ApplicationResource a base_url that stands on its own" do
+      install!
+
+      expect(generated("app/resources/application_resource.rb"))
+        .to include(%(self.base_url = ENV.fetch('BASE_URL', 'http://localhost:3000')))
+    end
+  end
+end
diff --git a/spec/integration/rails/locale_generator_spec.rb b/spec/integration/rails/locale_generator_spec.rb
new file mode 100644
index 00000000..dd13e3d5
--- /dev/null
+++ b/spec/integration/rails/locale_generator_spec.rb
@@ -0,0 +1,94 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  require "rails/generators"
+  require "generators/graphiti/locale_generator"
+  require "fileutils"
+  require "tmpdir"
+  require "yaml"
+
+  RSpec.describe Graphiti::LocaleGenerator do
+    let(:destination) { Dir.mktmpdir("graphiti-locale") }
+
+    def generated
+      File.read(File.join(destination, "config/locales/graphiti.en.yml"))
+    end
+
+    def generate!(*arguments)
+      described_class.start(
+        arguments,
+        destination_root: destination, shell: Thor::Shell::Basic.new
+      )
+    end
+
+    after { FileUtils.remove_entry(destination) }
+
+    it "writes every message Graphiti renders" do
+      generate!
+
+      locale = YAML.safe_load(generated)
+
+      expect(locale.dig("en", "graphiti", "errors", "messages"))
+        .to eq(Graphiti::Util::SimpleErrors::DEFAULT_MESSAGES.transform_keys(&:to_s))
+      expect(locale.dig("en", "graphiti", "errors", "format"))
+        .to eq(Graphiti::Util::SimpleErrors::DEFAULT_FORMAT)
+    end
+
+    it "writes the title Graphiti renders for every code" do
+      generate!
+
+      titles = YAML.safe_load(generated)
+        .dig("en", "graphiti", "errors")
+        .slice(*Graphiti::LocaleGenerator::ERROR_TITLES.keys.map(&:to_s))
+        .transform_values { |entry| entry["title"] }
+
+      expect(titles).to eq(Graphiti::LocaleGenerator::ERROR_TITLES.transform_keys(&:to_s))
+    end
+
+    it "names the exceptions behind each registered code" do
+      generate!
+
+      expect(generated).to include("# RecordNotFound")
+      expect(generated).to include("# ConflictRequest")
+      expect(generated).to include("# InvalidRequest, RemoteWrite,")
+    end
+
+    it "names every registered exception, so a new one cannot go undocumented" do
+      generate!
+
+      Graphiti::Rails::CLIENT_ERROR_STATUSES.each_key do |exception|
+        expect(generated).to include(exception.delete_prefix("Graphiti::Errors::"))
+      end
+    end
+
+    it "gives the fallback a detail, since nothing else reports one for it" do
+      generate!
+
+      expect(YAML.safe_load(generated).dig("en", "graphiti", "errors", "internal_server_error"))
+        .to eq(
+          "title" => "Internal Server Error",
+          "detail" => "We've probably received an error report already, but please contact us if the issue persists."
+        )
+    end
+
+    it "writes a detail for no other code" do
+      generate!
+
+      codes = YAML.safe_load(generated).dig("en", "graphiti", "errors")
+
+      expect(codes.select { |_code, entry| entry.is_a?(Hash) && entry.key?("detail") }.keys)
+        .to eq(["internal_server_error"])
+    end
+
+    it "still writes the detail with comments omitted" do
+      generate!("--omit-comments")
+
+      expect(YAML.safe_load(generated).dig("en", "graphiti", "errors", "internal_server_error", "detail"))
+        .to eq("We've probably received an error report already, but please contact us if the issue persists.")
+    end
+
+    it "omits the comments when asked" do
+      generate!("--omit-comments")
+
+      expect(generated).to_not include("#")
+    end
+  end
+end
diff --git a/spec/integration/rails/locale_spec.rb b/spec/integration/rails/locale_spec.rb
new file mode 100644
index 00000000..2f568bf2
--- /dev/null
+++ b/spec/integration/rails/locale_spec.rb
@@ -0,0 +1,49 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  require "rails_spec_helper"
+
+  RSpec.describe "I18n.locale inside concurrent sideloads" do
+    include ConcurrencyHarness
+
+    let!(:employee) { Employee.create!(first_name: "Jane") }
+
+    before do
+      allow(Graphiti.config).to receive(:concurrency).and_return(true)
+      with_thread_pool(max_threads: 2)
+    end
+
+    around do |example|
+      available = I18n.available_locales
+      I18n.available_locales = available | [:de]
+      example.run
+    ensure
+      I18n.locale = I18n.default_locale
+      I18n.available_locales = available
+    end
+
+    it "resolves a sideload in the locale the request set" do
+      observed = nil
+
+      resource_class = Class.new(EmployeeResource) do
+        def self.name
+          "EmployeeResource"
+        end
+      end
+      resource_class.has_many :probe_positions, resource: PositionResource, foreign_key: :employee_id do
+        scope do |_employee_ids|
+          observed = {thread: Thread.current.object_id, locale: I18n.locale}
+          Position.none
+        end
+      end
+
+      resource_class.has_many :other_positions, resource: PositionResource, foreign_key: :employee_id do
+        scope { |_employee_ids| Position.none }
+      end
+
+      I18n.locale = :de
+      resource_class.all(filter: {id: employee.id}, include: "probe_positions,other_positions").to_a
+
+      expect(observed[:thread]).to_not eq(Thread.current.object_id)
+      expect(observed[:locale]).to eq(:de)
+    end
+  end
+end
diff --git a/spec/integration/rails/persistence_spec.rb b/spec/integration/rails/persistence_spec.rb
index a7cd55b9..9cb8e585 100644
--- a/spec/integration/rails/persistence_spec.rb
+++ b/spec/integration/rails/persistence_spec.rb
@@ -1,6 +1,6 @@
 if ENV["APPRAISAL_INITIALIZED"]
   RSpec.describe "persistence", type: :controller do
-    include GraphitiSpecHelpers
+    include Graphiti::SpecHelpers
 
     # defined in spec/supports/rails/employee_controller.rb
     controller(ApplicationController, &EMPLOYEE_CONTROLLER_BLOCK)
@@ -109,7 +109,7 @@
       context "when reserved parameter used" do
         before do
           resource = Class.new(EmployeeResource) do
-            self.validate_endpoints = false
+            self.validate_requests = false
             attribute :page, :integer
           end
           allow(controller).to receive(:resource) { resource }
@@ -263,7 +263,7 @@ def self.name
       context "when has_many" do
         let(:klass) do
           Class.new(EmployeeResource) do
-            self.validate_endpoints = false
+            self.validate_requests = false
           end
         end
 
@@ -283,7 +283,7 @@ def self.name
               relationships: {
                 positions: {
                   data: [{
-                    'temp-id': "abc123",
+                    "temp-id": "abc123",
                     type: "positions",
                     method: "create"
                   }]
@@ -292,7 +292,7 @@ def self.name
             },
             included: [
               {
-                'temp-id': "abc123",
+                "temp-id": "abc123",
                 type: "positions",
                 attributes: {title: "foo"}
               }
@@ -310,7 +310,7 @@ def self.name
             make_request
           }.to(raise_error { |e|
             expect(e).to be_a Graphiti::Errors::InvalidRequest
-            expect(e.errors.full_messages).to eq ["data.relationships.positions is unwritable relationship"]
+            expect(e.errors.full_messages).to eq ["data.relationships.positions cannot be written"]
           })
         end
       end
@@ -318,7 +318,7 @@ def self.name
       context "when belongs_to" do
         let(:klass) do
           Class.new(EmployeeResource) do
-            self.validate_endpoints = false
+            self.validate_requests = false
             belongs_to :classification, writable: false
           end
         end
@@ -352,7 +352,7 @@ def self.name
             make_request
           }.to(raise_error { |e|
             expect(e).to be_a Graphiti::Errors::InvalidRequest
-            expect(e.errors.full_messages).to eq ["data.relationships.classification is unwritable relationship"]
+            expect(e.errors.full_messages).to eq ["data.relationships.classification cannot be written"]
           })
         end
       end
@@ -363,7 +363,7 @@ def self.name
 
       let(:klass) do
         Class.new(EmployeeResource) do
-          self.validate_endpoints = false
+          self.validate_requests = false
 
           after_graph_persist do |model|
             model.valid?(:after_graph_persisted)
@@ -390,20 +390,20 @@ def self.model_name
             relationships: {
               positions: {
                 data: [
-                  {'temp-id': "pos1", type: "positions", method: "create"},
-                  {'temp-id': "pos2", type: "positions", method: "create"}
+                  {"temp-id": "pos1", type: "positions", method: "create"},
+                  {"temp-id": "pos2", type: "positions", method: "create"}
                 ]
               }
             }
           },
           included: [
             {
-              'temp-id': "pos1",
+              "temp-id": "pos1",
               type: "positions",
               attributes: {title: "foo"}
             },
             {
-              'temp-id': "pos2",
+              "temp-id": "pos2",
               type: "positions",
               attributes: {title: "bar"}
             }
@@ -450,7 +450,7 @@ def self.model_name
       context "when belongs_to" do
         let(:klass) do
           Class.new(EmployeeResource) do
-            self.validate_endpoints = false
+            self.validate_requests = false
             attribute :classification_id, :integer, writable: false
           end
         end
@@ -518,7 +518,7 @@ def self.name
       context "when has_many" do
         let(:klass) do
           Class.new(EmployeeResource) do
-            self.validate_endpoints = false
+            self.validate_requests = false
           end
         end
 
@@ -541,7 +541,7 @@ def self.name
               relationships: {
                 positions: {
                   data: [{
-                    'temp-id': "abc123",
+                    "temp-id": "abc123",
                     type: "positions",
                     method: "create"
                   }]
@@ -550,7 +550,7 @@ def self.name
             },
             included: [
               {
-                'temp-id': "abc123",
+                "temp-id": "abc123",
                 type: "positions",
                 attributes: {title: "foo"}
               }
@@ -604,7 +604,7 @@ def self.name
               relationships: {
                 salary: {
                   data: {
-                    'temp-id': "abc123",
+                    "temp-id": "abc123",
                     type: "salaries",
                     method: "create"
                   }
@@ -613,7 +613,7 @@ def self.name
             },
             included: [
               {
-                'temp-id': "abc123",
+                "temp-id": "abc123",
                 type: "salaries",
                 attributes: {
                   base_rate: 15.00,
@@ -790,13 +790,13 @@ def self.name
             relationships: {
               positions: {
                 data: [
-                  {type: "positions", 'temp-id': "pos1", method: "create"},
-                  {type: "positions", 'temp-id': "pos2", method: "create"}
+                  {type: "positions", "temp-id": "pos1", method: "create"},
+                  {type: "positions", "temp-id": "pos2", method: "create"}
                 ]
               },
               teams: {
                 data: [
-                  {type: "teams", 'temp-id': "team1", method: "create"}
+                  {type: "teams", "temp-id": "team1", method: "create"}
                 ]
               }
             }
@@ -804,27 +804,27 @@ def self.name
           included: [
             {
               type: "positions",
-              'temp-id': "pos1",
+              "temp-id": "pos1",
               attributes: {title: "specialist"},
               relationships: {
                 department: {
-                  data: {type: "departments", 'temp-id': "dep1", method: "create"}
+                  data: {type: "departments", "temp-id": "dep1", method: "create"}
                 }
               }
             },
             {
               type: "departments",
-              'temp-id': "dep1",
+              "temp-id": "dep1",
               attributes: {name: "safety"}
             },
             {
               type: "positions",
-              'temp-id': "pos2",
+              "temp-id": "pos2",
               attributes: {title: "manager"}
             },
             {
               type: "teams",
-              'temp-id': "team1",
+              "temp-id": "team1",
               attributes: {name: "Team 1"}
             }
           ]
@@ -1166,9 +1166,9 @@ def self.name
       # NB - should only sideload updated position, not all positions
       it "sideloads the objects in response" do
         make_request
-        expect(included("positions").length).to eq(1)
-        expect(included("positions")[0].id).to eq(position2.id)
-        expect(included("departments").length).to eq(1)
+        expect(jsonapi_included("positions").length).to eq(1)
+        expect(jsonapi_included("positions")[0].id).to eq(position2.id)
+        expect(jsonapi_included("departments").length).to eq(1)
       end
     end
 
@@ -1268,26 +1268,26 @@ def self.name
             relationships: {
               positions: {
                 data: [
-                  {'temp-id': "a", type: "positions", method: "create"}
+                  {"temp-id": "a", type: "positions", method: "create"}
                 ]
               }
             }
           },
           included: [
             {
-              'temp-id': "a",
+              "temp-id": "a",
               type: "positions",
               attributes: {},
               relationships: {
                 department: {
                   data: {
-                    'temp-id': "b", type: "departments", method: "create"
+                    "temp-id": "b", type: "departments", method: "create"
                   }
                 }
               }
             },
             {
-              'temp-id': "b",
+              "temp-id": "b",
               type: "departments",
               attributes: {}
             }
@@ -1385,7 +1385,7 @@ def self.name
             relationships: {
               teams: {
                 data: [
-                  {'temp-id': "abc123", type: "teams", method: "create"},
+                  {"temp-id": "abc123", type: "teams", method: "create"},
                   {id: prior_team.id.to_s, type: "teams", method: "update"},
                   {id: disassociate_team.id.to_s, type: "teams", method: "disassociate"},
                   {id: destroy_team.id.to_s, type: "teams", method: "destroy"},
@@ -1396,7 +1396,7 @@ def self.name
           },
           included: [
             {
-              'temp-id': "abc123",
+              "temp-id": "abc123",
               type: "teams",
               attributes: {name: "Team #1"}
             },
@@ -1447,20 +1447,20 @@ def self.name
             relationships: {
               tasks: {
                 data: [
-                  {'temp-id': "abc123", type: "features", method: "create"},
-                  {'temp-id': "abc456", type: "bugs", method: "create"}
+                  {"temp-id": "abc123", type: "features", method: "create"},
+                  {"temp-id": "abc456", type: "bugs", method: "create"}
                 ]
               }
             }
           },
           included: [
             {
-              'temp-id': "abc123",
+              "temp-id": "abc123",
               type: "features",
               attributes: {name: "test feature"}
             },
             {
-              'temp-id': "abc456",
+              "temp-id": "abc456",
               type: "bugs",
               attributes: {name: "test bug"}
             }
@@ -1507,7 +1507,7 @@ def self.name
 
       context "when creating" do
         let(:location_id) { "abc123" }
-        let(:location_id_key) { :'temp-id' }
+        let(:location_id_key) { :"temp-id" }
         let(:method) { "create" }
 
         it "works" do
@@ -1595,7 +1595,7 @@ def self.name
 
       context "when creating" do
         let(:note_id) { "abc123" }
-        let(:note_id_key) { :'temp-id' }
+        let(:note_id_key) { :"temp-id" }
         let(:method) { "create" }
 
         it "works" do
@@ -1668,7 +1668,7 @@ def self.name
             relationships: {
               workspace: {
                 data: {
-                  'temp-id': "work1", type: workspace_type, method: "create"
+                  "temp-id": "work1", type: workspace_type, method: "create"
                 }
               }
             }
@@ -1676,7 +1676,7 @@ def self.name
           included: [
             {
               type: workspace_type,
-              'temp-id': "work1",
+              "temp-id": "work1",
               attributes: {
                 address: "Fake Workspace Address"
               }
@@ -1748,4 +1748,78 @@ def self.name
       end
     end
   end
+
+  # Applying the payload without saving lets a controller inspect ActiveRecord's
+  # dirty tracking to decide whether the update is permitted at all.
+  # type: :controller only to opt into the DatabaseCleaner hooks in spec_helper
+  RSpec.describe "applying a payload without saving", type: :controller do
+    let!(:employee) { Employee.create!(first_name: "Joe", last_name: "Smith") }
+
+    let(:payload) do
+      {
+        id: employee.id,
+        data: {
+          id: employee.id.to_s,
+          type: "employees",
+          attributes: {first_name: "Jane"}
+        }
+      }
+    end
+
+    around do |e|
+      Graphiti.with_context({}, :update) { e.run }
+    end
+
+    it "leaves the record clean before the payload is applied" do
+      proxy = EmployeeResource.find(payload)
+
+      expect(proxy.data.changed).to eq([])
+      expect(proxy.data.first_name).to eq("Joe")
+    end
+
+    it "reports the changed attributes once the payload is applied" do
+      proxy = EmployeeResource.find(payload)
+      proxy.assign_attributes(payload)
+
+      expect(proxy.data.changed).to eq(["first_name"])
+      expect(proxy.data.first_name_was).to eq("Joe")
+      expect(proxy.data).to be_changed
+    end
+
+    it "does not touch the database" do
+      proxy = EmployeeResource.find(payload)
+      proxy.assign_attributes(payload)
+
+      expect(employee.reload.first_name).to eq("Joe")
+    end
+
+    it "reports the same changes when applied more than once" do
+      proxy = EmployeeResource.find(payload)
+      proxy.assign_attributes(payload)
+      proxy.assign_attributes(payload)
+
+      expect(proxy.data.changed).to eq(["first_name"])
+    end
+
+    it "commits the pending changes on update_attributes" do
+      proxy = EmployeeResource.find(payload)
+      proxy.assign_attributes(payload)
+
+      expect {
+        expect(proxy.update_attributes).to eq(true)
+      }.to change { employee.reload.first_name }.from("Joe").to("Jane")
+
+      expect(proxy.data.changed).to eq([])
+    end
+
+    it "surfaces validation errors without saving" do
+      payload[:data][:attributes][:first_name] = nil
+      proxy = EmployeeResource.find(payload)
+      proxy.assign_attributes(payload)
+
+      expect(proxy.data.valid?).to eq(false)
+      expect(proxy.data.errors.full_messages).to eq(["First name can't be blank"])
+      expect(employee.reload.first_name).to eq("Joe")
+    end
+  end
 end
diff --git a/spec/integration/rails/persistence_with_finders_spec.rb b/spec/integration/rails/persistence_with_finders_spec.rb
index 9ad8d1c5..04035575 100644
--- a/spec/integration/rails/persistence_with_finders_spec.rb
+++ b/spec/integration/rails/persistence_with_finders_spec.rb
@@ -87,13 +87,13 @@
           {
             data: {
               type: "employees",
-              'temp-id': "empl1",
+              "temp-id": "empl1",
               attributes: {first_name: "Joe"},
               relationships: {
                 positions: {
                   data: [
-                    {type: "positions", 'temp-id': "pos1", method: "create"},
-                    {type: "positions", 'temp-id': "pos2", method: "create"}
+                    {type: "positions", "temp-id": "pos1", method: "create"},
+                    {type: "positions", "temp-id": "pos2", method: "create"}
                   ]
                 }
               }
@@ -101,22 +101,22 @@
             included: [
               {
                 type: "positions",
-                'temp-id': "pos1",
+                "temp-id": "pos1",
                 attributes: {title: "specialist"},
                 relationships: {
                   department: {
-                    data: {type: "departments", 'temp-id': "dep1", method: "create"}
+                    data: {type: "departments", "temp-id": "dep1", method: "create"}
                   }
                 }
               },
               {
                 type: "departments",
-                'temp-id': "dep1",
+                "temp-id": "dep1",
                 attributes: {name: "safety"}
               },
               {
                 type: "positions",
-                'temp-id': "pos2",
+                "temp-id": "pos2",
                 attributes: {title: "manager"}
               }
             ]
@@ -174,7 +174,7 @@
           it "allows sideloading nested records" do
             make_request
 
-            expect(included("departments")[0].attributes).to eq({
+            expect(jsonapi_included("departments")[0].attributes).to eq({
               "id" => department.id.to_s,
               "name" => department.name,
               "jsonapi_type" => "departments"
@@ -238,7 +238,7 @@
         it "includes requested sideloads and their requested fields" do
           make_request
 
-          expect(included("salaries").first.attributes).to eq({
+          expect(jsonapi_included("salaries").first.attributes).to eq({
             "id" => salary.id.to_s,
             "jsonapi_type" => "salaries",
             "base_rate" => 80.0
@@ -321,10 +321,10 @@
           it "sideloads the updated objects plus included objects" do
             make_request
 
-            expect(included("teams").length).to eq(2)
-            expect(included("positions").length).to eq(2)
-            expect(included("departments").length).to eq(1)
-            expect(included("salaries").length).to eq(1)
+            expect(jsonapi_included("teams").length).to eq(2)
+            expect(jsonapi_included("positions").length).to eq(2)
+            expect(jsonapi_included("departments").length).to eq(1)
+            expect(jsonapi_included("salaries").length).to eq(1)
           end
         end
       end
diff --git a/spec/integration/rails/railtie_spec.rb b/spec/integration/rails/railtie_spec.rb
new file mode 100644
index 00000000..6f9ba85e
--- /dev/null
+++ b/spec/integration/rails/railtie_spec.rb
@@ -0,0 +1,96 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  # Graphiti.config's own Rails defaults (schema_path, logger, debug) are covered
+  # in spec/configuration_spec.rb. This covers what the Railtie registers with
+  # Rails on boot.
+  RSpec.describe Graphiti::Rails::Railtie do
+    describe "the jsonapi mime type" do
+      it "is registered" do
+        expect(Mime[:jsonapi].to_s).to eq("application/vnd.api+json")
+      end
+    end
+
+    describe "the jsonapi parameter parser" do
+      subject(:parser) { ActionDispatch::Request.parameter_parsers[:jsonapi] }
+
+      it "is registered" do
+        expect(parser).to be_a(Proc)
+      end
+
+      it "parses a jsonapi body into params readable by string or symbol" do
+        parsed = parser.call({data: {type: "authors"}}.to_json)
+
+        expect(parsed[:data][:type]).to eq("authors")
+        expect(parsed["data"]["type"]).to eq("authors")
+      end
+    end
+
+    describe "renderers" do
+      it "registers jsonapi and jsonapi_errors" do
+        expect(ActionController::Renderers::RENDERERS)
+          .to include(:jsonapi, :jsonapi_errors)
+      end
+    end
+
+    describe "config.graphiti options" do
+      it "defaults handled_exception_formats to jsonapi" do
+        expect(Graphiti::Rails.handled_exception_formats).to eq([:jsonapi])
+      end
+
+      it "defaults respond_to_formats to json, jsonapi and xml" do
+        expect(Graphiti::Rails.respond_to_formats)
+          .to eq([:json, :jsonapi, :xml])
+      end
+    end
+
+    describe "endpoint lookup" do
+      it "is configured" do
+        expect(Graphiti.config.context_for_endpoint).to be_a(Proc)
+      end
+    end
+
+    describe "rescue_responses" do
+      let(:controller) do
+        Class.new(ActionController::Base) { include Graphiti::Rails::Controller }
+      end
+
+      it "tells Rails which Graphiti errors are client errors" do
+        expect(ActionDispatch::ExceptionWrapper.rescue_responses)
+          .to include(Graphiti::Rails::CLIENT_ERROR_STATUSES)
+      end
+
+      it "keeps them out of the error reporter" do
+        wrapper = ActionDispatch::ExceptionWrapper
+          .new(nil, Graphiti::Errors::RecordNotFound.new)
+
+        expect(wrapper.rescue_response?).to eq(true)
+      end
+
+      it "gives each the status the controller renders" do
+        Graphiti::Rails::CLIENT_ERROR_STATUSES.each do |name, status|
+          registered = controller.rescue_registry
+            .status_code_for_exception(name.constantize, passthrough: false)
+
+          expect(registered).to eq(Rack::Utils.status_code(status)), name
+        end
+      end
+
+      it "covers every client error the controller registers" do
+        handlers = controller.rescue_registry.instance_variable_get(:@handlers)
+        client_errors = handlers.select { |_, (_, options)|
+          (400..499).cover?(options[:status])
+        }
+
+        expect(client_errors.keys.map(&:name))
+          .to match_array(Graphiti::Rails::CLIENT_ERROR_STATUSES.keys)
+      end
+    end
+
+    describe "rake tasks" do
+      it "ships the graphiti tasks the Railtie loads" do
+        task_path = File.expand_path("../../../lib/tasks/graphiti.rake", __dir__)
+
+        expect(File.exist?(task_path)).to eq(true)
+      end
+    end
+  end
+end
diff --git a/spec/integration/rails/register_exception_reach_spec.rb b/spec/integration/rails/register_exception_reach_spec.rb
new file mode 100644
index 00000000..18eff967
--- /dev/null
+++ b/spec/integration/rails/register_exception_reach_spec.rb
@@ -0,0 +1,51 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  # register_exception comes from rescue_registry, which adds it to every
+  # controller, and rendering happens in its middleware. Neither needs
+  # Graphiti::Rails::Controller. What that adds is Graphiti's own
+  # registrations and the fallback for exceptions nobody registered.
+  RSpec.describe "register_exception without Graphiti::Rails::Controller", type: :request do
+    include Graphiti::Rails::TestHelpers
+
+    class BareForbidden < StandardError; end
+
+    class BareErrorsController < ActionController::Base
+      register_exception BareForbidden, status: 403
+
+      def registered
+        raise BareForbidden
+      end
+
+      def unregistered
+        raise "kaboom"
+      end
+    end
+
+    before do
+      Rails.application.routes.draw do
+        get "/bare/registered" => "bare_errors#registered"
+        get "/bare/unregistered" => "bare_errors#unregistered"
+      end
+    end
+
+    def get_jsonapi(path)
+      handle_request_exceptions do
+        get path, headers: {"HTTP_ACCEPT" => "application/vnd.api+json"}
+      end
+    end
+
+    it "registers and renders an exception the controller declared" do
+      get_jsonapi "/bare/registered"
+
+      expect(response.status).to eq(403)
+      expect(response.content_type).to start_with("application/vnd.api+json")
+      expect(JSON.parse(response.body)["errors"][0]["code"]).to eq("forbidden")
+    end
+
+    it "leaves unregistered exceptions to Rails, having no Graphiti fallback" do
+      get_jsonapi "/bare/unregistered"
+
+      expect(response.status).to eq(500)
+      expect(response.content_type).to_not include("vnd.api+json")
+    end
+  end
+end
diff --git a/spec/integration/rails/resource_generator_spec.rb b/spec/integration/rails/resource_generator_spec.rb
new file mode 100644
index 00000000..022313e1
--- /dev/null
+++ b/spec/integration/rails/resource_generator_spec.rb
@@ -0,0 +1,182 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  require "rails/generators"
+  require "generators/graphiti/resource_generator"
+  require "fileutils"
+  require "tmpdir"
+
+  # graphiti-rails checked its generators' output into its own suite as
+  # spec/generated/** and ran that against a dummy app. Those specs re-tested
+  # behavior this suite already covers at length. What they really stood in for
+  # is that the templates still produce sane code. This runs the generator
+  # rather than shipping its output.
+  RSpec.describe Graphiti::ResourceGenerator do
+    let(:destination) { Dir.mktmpdir("graphiti-generator") }
+
+    def generated(path)
+      File.read(File.join(destination, path))
+    end
+
+    def generate!(*arguments)
+      Dir.chdir(destination) do
+        described_class.start(
+          arguments,
+          destination_root: destination, shell: Thor::Shell::Basic.new
+        )
+      end
+    end
+
+    before do
+      stub_const("Post", Class.new)
+      # The generator requires what it writes, relative to Rails.root, so each
+      # run defines PostResource for real. Hidden per example so the next run
+      # does not hit a superclass mismatch against the previous definition.
+      allow(::Rails).to receive(:root).and_return(Pathname.new(destination))
+      hide_const("PostResource")
+
+      FileUtils.mkdir_p(File.join(destination, "config"))
+      # The shape apps installed before 2.0 still have. Resource routes are
+      # injected after the scope line, matched on its format default.
+      File.write(File.join(destination, "config/routes.rb"), <<~RUBY)
+        Rails.application.routes.draw do
+          scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
+          end
+        end
+      RUBY
+      # Present so the generator does not prompt for an API namespace on stdin.
+      File.write(File.join(destination, ".graphiticfg.yml"), {"namespace" => "/api/v1"}.to_yaml)
+    end
+
+    after { FileUtils.remove_entry(destination) }
+
+    context "when the app already has an ApplicationResource" do
+      before do
+        stub_const("ApplicationResource", Class.new(Graphiti::Resource) {
+          self.abstract_class = true
+          self.endpoint_namespace = "/api/v1"
+        })
+
+        generate!("Post", "title:string", "upvotes:integer")
+      end
+
+      it "generates a resource with the requested attributes and types" do
+        resource = generated("app/resources/post_resource.rb")
+
+        expect(resource).to include("class PostResource < ApplicationResource")
+        expect(resource).to include("attribute :title, :string")
+        expect(resource).to include("attribute :upvotes, :integer")
+      end
+
+      it "generates a controller wired to the resource" do
+        controller = generated("app/controllers/posts_controller.rb")
+
+        expect(controller).to include("class PostsController < ApplicationController")
+        expect(controller).to include("PostResource")
+      end
+
+      it "routes the resource under the configured namespace" do
+        expect(generated("config/routes.rb")).to include("resources :posts")
+      end
+
+      it "routes into the scope the current install generator writes" do
+        File.write(File.join(destination, "config/routes.rb"), <<~RUBY)
+          Rails.application.routes.draw do
+            scope path: "/api/v1", defaults: {format: :jsonapi} do
+            end
+          end
+        RUBY
+
+        generate!("Post", "title:string")
+
+        expect(generated("config/routes.rb")).to include("resources :posts")
+      end
+
+      it "generates resource specs" do
+        expect(generated("spec/resources/post/reads_spec.rb"))
+          .to include("RSpec.describe PostResource, type: :resource")
+        expect(File).to exist(File.join(destination, "spec/resources/post/writes_spec.rb"))
+      end
+
+      it "generates request specs under the namespace" do
+        %w[index show create update destroy].each do |action|
+          expect(File).to exist(File.join(destination, "spec/api/v1/posts/#{action}_spec.rb"))
+        end
+      end
+
+      it "does not generate a second ApplicationResource" do
+        expect(File).to_not exist(File.join(destination, "app/resources/application_resource.rb"))
+      end
+    end
+
+    context "when --controller is given" do
+      before do
+        stub_const("ApplicationResource", Class.new(Graphiti::Resource) {
+          self.abstract_class = true
+          self.endpoint_namespace = "/api/v1"
+        })
+      end
+
+      it "generates the controller under that name" do
+        generate!("Post", "title:string", "--controller", "Api::V1::Posts")
+
+        controller = generated("app/controllers/api/v1/posts_controller.rb")
+        expect(controller).to include("class Api::V1::PostsController < ApplicationController")
+        expect(controller).to include("PostResource")
+      end
+
+      it "points the route at it, since the name no longer matches the resource" do
+        generate!("Post", "title:string", "--controller", "Api::V1::Posts")
+
+        expect(generated("config/routes.rb"))
+          .to include(%(resources :posts, controller: "api/v1/posts"))
+      end
+
+      it "accepts the name with or without the Controller suffix" do
+        generate!("Post", "title:string", "--controller", "Api::V1::PostsController")
+
+        expect(generated("app/controllers/api/v1/posts_controller.rb"))
+          .to include("class Api::V1::PostsController")
+      end
+
+      it "singularizes to the same controller a plural name would give" do
+        generate!("Post", "title:string", "--controller", "Api::V1::Post")
+
+        expect(generated("app/controllers/api/v1/posts_controller.rb"))
+          .to include("class Api::V1::PostsController")
+      end
+
+      it "leaves the resource itself unnamespaced" do
+        generate!("Post", "title:string", "--controller", "Api::V1::Posts")
+
+        expect(generated("app/resources/post_resource.rb"))
+          .to include("class PostResource < ApplicationResource")
+      end
+    end
+
+    context "when the app has no ApplicationResource yet" do
+      before do
+        hide_const("ApplicationResource")
+
+        generate!("Post", "title:string", "--actions", "index", "show")
+      end
+
+      it "generates one to inherit from" do
+        expect(generated("app/resources/application_resource.rb"))
+          .to include("class ApplicationResource < Graphiti::Resource")
+      end
+
+      it "documents the inherited settings with their defaults and options" do
+        contents = generated("app/resources/application_resource.rb")
+
+        expect(contents).to include("  # Links\n")
+        expect(contents).to include("# self.relationship_links                 = true          # true, false, or :on_demand")
+        expect(contents).to include("# self.belongs_to_resource_ids_by_default = :foreign_key  # :foreign_key, :always, or :never")
+      end
+
+      it "only generates request specs for the requested actions" do
+        expect(File).to exist(File.join(destination, "spec/api/v1/posts/index_spec.rb"))
+        expect(File).to exist(File.join(destination, "spec/api/v1/posts/show_spec.rb"))
+        expect(File).to_not exist(File.join(destination, "spec/api/v1/posts/destroy_spec.rb"))
+      end
+    end
+  end
+end
diff --git a/spec/integration/rails/responders_spec.rb b/spec/integration/rails/responders_spec.rb
new file mode 100644
index 00000000..d8b40378
--- /dev/null
+++ b/spec/integration/rails/responders_spec.rb
@@ -0,0 +1,81 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  # Rails extracted respond_with into the responders gem. An app gets it from
+  # Bundler.require. The bare app this suite boots has to ask for it.
+  require "responders"
+
+  RSpec.describe Graphiti::Rails::Responders, type: :controller do
+    controller(ApplicationController) do
+      include Graphiti::Rails::Responders
+
+      def index
+        respond_with(Legacy::AuthorResource.all(params))
+      end
+    end
+
+    let!(:author) { Legacy::Author.create!(first_name: "Stephen") }
+
+    # The resource validates that the request path is one of its endpoints, and
+    # an anonymous controller has no route matching it.
+    before do
+      allow(controller.request.env).to receive(:[])
+        .with(anything).and_call_original
+      allow(controller.request.env).to receive(:[])
+        .with("PATH_INFO") { "/legacy/authors" }
+    end
+
+    it "responds to the configured formats" do
+      expect(controller.class.mimes_for_respond_to.keys)
+        .to match_array(Graphiti::Rails.respond_to_formats)
+    end
+
+    it "renders jsonapi through respond_with" do
+      request.accept = "application/vnd.api+json"
+
+      get :index
+
+      expect(response.status).to eq(200)
+      expect(JSON.parse(response.body)["data"].map { |d| d["id"] })
+        .to eq([author.id.to_s])
+    end
+
+    describe "the respond_with location override" do
+      # This is the only behaviour the module adds beyond declaring formats.
+      # Rails' responder builds a Location header for a created resource by
+      # calling polymorphic_url on it, which expects an ActiveModel. A
+      # Graphiti::ResourceProxy is not one.
+      controller(ApplicationController) do
+        include Graphiti::Rails::Responders
+
+        def create
+          respond_with(Legacy::AuthorResource.build(params).tap(&:save))
+        end
+      end
+
+      let(:payload) do
+        {data: {type: "authors", attributes: {first_name: "Stephen"}}}
+      end
+
+      it "creates without trying to generate a url for the proxy" do
+        request.accept = "application/vnd.api+json"
+
+        post :create, params: payload
+
+        expect(response.status).to eq(201)
+        expect(response.headers["Location"]).to be_nil
+      end
+
+      it "is what stops respond_with raising on the proxy" do
+        plain = Class.new(ApplicationController) do
+          include ActionController::MimeResponds
+
+          respond_to :jsonapi
+        end
+
+        expect(plain.instance_method(:respond_with).owner)
+          .to_not eq(Graphiti::Rails::Responders)
+        expect(described_class.instance_method(:respond_with).owner)
+          .to eq(described_class)
+      end
+    end
+  end
+end
diff --git a/spec/integration/rails/schema_tasks_spec.rb b/spec/integration/rails/schema_tasks_spec.rb
new file mode 100644
index 00000000..c1595ccc
--- /dev/null
+++ b/spec/integration/rails/schema_tasks_spec.rb
@@ -0,0 +1,51 @@
+if ENV["APPRAISAL_INITIALIZED"]
+  require "rake"
+  require "tmpdir"
+
+  RSpec.describe "the graphiti:schema rake tasks" do
+    let(:path) { File.join(@dir, "nested", "schema.json") }
+
+    # The suite defines resources the generator cannot resolve.
+    before do
+      allow(Graphiti::Schema).to receive(:generate)
+        .and_return({resources: [], endpoints: {}, types: {}})
+    end
+
+    around do |example|
+      original = Rake.application
+      Rake.application = Rake::Application.new
+      Rake::Task.define_task(:environment)
+      load File.expand_path("../../../lib/tasks/graphiti.rake", __dir__)
+
+      Dir.mktmpdir do |dir|
+        @dir = dir
+        example.run
+      end
+    ensure
+      Rake.application = original
+    end
+
+    def run(task)
+      Rake::Task[task].invoke(path)
+    end
+
+    it "generates the file at the given path" do
+      expect { run("graphiti:schema:generate") }.to output(/Schema written/).to_stdout
+
+      expect(JSON.parse(File.read(path))).to include("resources")
+    end
+
+    it "checks a file it just generated" do
+      expect { run("graphiti:schema:generate") }.to output.to_stdout
+      Rake::Task["graphiti:schema:generate"].reenable
+
+      expect { run("graphiti:schema:check") }.to output(/up to date/).to_stdout
+    end
+
+    it "fails the check when the file is missing" do
+      expect { run("graphiti:schema:check") }
+        .to raise_error(SystemExit)
+        .and output(/Schema file not found/).to_stderr
+    end
+  end
+end
diff --git a/spec/integration/rails/sunspot_pattern_spec.rb b/spec/integration/rails/sunspot_pattern_spec.rb
index 262fb2c7..c029b9ab 100644
--- a/spec/integration/rails/sunspot_pattern_spec.rb
+++ b/spec/integration/rails/sunspot_pattern_spec.rb
@@ -9,7 +9,7 @@
   #
   # This scenario is likely specific to ActiveRecord.
   RSpec.describe "a non-ActiveRecord adapter that returns ActiveRecord models", type: :controller do
-    include GraphitiSpecHelpers
+    include Graphiti::SpecHelpers
 
     controller(ApplicationController) do
       def index
@@ -35,13 +35,13 @@ def index
 
     it "works" do
       do_index({include: "special_books"})
-      expect(d[0].sideload(:special_books).map(&:id)).to eq([book.id])
+      expect(jsonapi_data[0].sideload(:special_books).map(&:id)).to eq([book.id])
     end
 
     context "belongs_to" do
       it "works" do
         do_index({include: "special_state"})
-        expect(d[0].sideload(:special_state).id).to eq(state.id)
+        expect(jsonapi_data[0].sideload(:special_state).id).to eq(state.id)
       end
     end
   end
diff --git a/spec/integration/rails/transaction_hooks_spec.rb b/spec/integration/rails/transaction_hooks_spec.rb
index d76aa098..abb6e03f 100644
--- a/spec/integration/rails/transaction_hooks_spec.rb
+++ b/spec/integration/rails/transaction_hooks_spec.rb
@@ -274,7 +274,7 @@ def json
             relationships: {
               positions: {
                 data: [
-                  {'temp-id': "a", type: "positions", method: "create"}
+                  {"temp-id": "a", type: "positions", method: "create"}
                 ]
               }
             }
@@ -282,18 +282,18 @@ def json
           included: [
             {
               type: "positions",
-              'temp-id': "a",
+              "temp-id": "a",
               relationships: {
                 department: {
                   data: {
-                    'temp-id': "b", type: "departments", method: "create"
+                    "temp-id": "b", type: "departments", method: "create"
                   }
                 }
               }
             },
             {
               type: "departments",
-              'temp-id': "b"
+              "temp-id": "b"
             }
           ]
         }
@@ -351,7 +351,7 @@ class << self
             attr_accessor :meta
           end
           self.model = ::Employee
-          self.validate_endpoints = false
+          self.validate_requests = false
 
           attribute :first_name, :string
 
@@ -383,7 +383,7 @@ class << self
             relationships: {
               positions: {
                 data: {
-                  type: "positions", 'temp-id': "abc123", method: "create"
+                  type: "positions", "temp-id": "abc123", method: "create"
                 }
               }
             }
@@ -391,7 +391,7 @@ class << self
           included: [
             {
               type: "positions",
-              'temp-id': "abc123",
+              "temp-id": "abc123",
               attributes: {title: "foo"}
             }
           ]
diff --git a/spec/no_rails_spec.rb b/spec/no_rails_spec.rb
new file mode 100644
index 00000000..8cde55cb
--- /dev/null
+++ b/spec/no_rails_spec.rb
@@ -0,0 +1,90 @@
+require "bundler"
+
+RSpec.describe "graphiti without Rails" do
+  describe "the gemspec" do
+    let(:gemspec) do
+      Gem::Specification.load(File.expand_path("../graphiti.gemspec", __dir__))
+    end
+
+    # The Rails integration ships inside this gem as of 2.0. It has to stay
+    # opt-in at load time rather than becoming a dependency, or every non-Rails
+    # user installs Rails to use Graphiti.
+    it "does not depend on Rails" do
+      runtime = gemspec.dependencies
+        .select { |dependency| dependency.type == :runtime }
+        .map(&:name)
+
+      expect(runtime).to_not include("rails", "railties", "actionpack", "activerecord")
+    end
+  end
+
+  # Only meaningful in the run that has no Rails in the bundle at all: the
+  # plain Gemfile rows of the CI matrix.
+  if !defined?(::Rails)
+    describe "loading" do
+      it "does not define the Rails integration" do
+        expect(defined?(Graphiti::Rails)).to be_nil
+      end
+
+      it "ships the Rails integration without loading it" do
+        expect(File).to exist(File.expand_path("../lib/graphiti/rails.rb", __dir__))
+      end
+
+      it "does not pull ActionController in" do
+        expect(defined?(::ActionController::Base)).to be_nil
+      end
+    end
+
+    describe "a process that requires nothing but graphiti" do
+      # This suite loads active_model, active_record and more, any of which
+      # pulls in enough of ActiveSupport to hide a missing require in
+      # lib/graphiti.rb. A bare subprocess is the only way to see what an app
+      # that requires graphiti and nothing else actually gets.
+      it "renders" do
+        script = <<~RUBY
+          require "graphiti"
+
+          class Post < Graphiti::Resource
+            self.adapter = Graphiti::Adapters::Null
+            self.model = Struct.new(:id, :title, keyword_init: true)
+            self.validate_requests = false
+            attribute :title, :string
+            def base_scope = {}
+            def resolve(_scope) = [model.new(id: 1, title: "Bare")]
+          end
+
+          print Graphiti.with_context(Object.new, :index) { Post.all({}).to_jsonapi }
+        RUBY
+
+        output = nil
+        gem_path = [Bundler.bundle_path.to_s, *Gem.path].uniq.join(File::PATH_SEPARATOR)
+        # Without this the child inherits RUBYOPT=-rbundler/setup and loads this
+        # suite's bundle, which is the opposite of what the test is checking.
+        Bundler.with_unbundled_env do
+          IO.popen([{"GEM_PATH" => gem_path}, "ruby", "-I", File.expand_path("../lib", __dir__), "-e", script], err: [:child, :out]) do |io|
+            output = io.read
+          end
+        end
+
+        expect($?).to be_success, "subprocess failed:\n#{output}"
+        expect(JSON.parse(output)["data"][0]["attributes"]["title"]).to eq("Bare")
+      end
+    end
+
+    describe "the library itself" do
+      it "resolves resources and renders without Rails" do
+        resource = Class.new(PORO::EmployeeResource) do
+          def self.name
+            "PORO::EmployeeResource"
+          end
+        end
+        PORO::Employee.create(first_name: "Jane")
+
+        rendered = JSON.parse(resource.all({}).to_jsonapi)
+
+        expect(rendered["data"].map { |d| d["attributes"]["first_name"] })
+          .to eq(["Jane"])
+      end
+    end
+  end
+end
diff --git a/spec/pagination_spec.rb b/spec/pagination_spec.rb
index 915a95de..6ef9f12f 100644
--- a/spec/pagination_spec.rb
+++ b/spec/pagination_spec.rb
@@ -16,7 +16,7 @@
 
   it "applies default pagination" do
     resource.class_eval do
-      self.default_page_size = 2
+      self.page_default_size = 2
     end
     expect(ids.length).to eq(2)
   end
@@ -24,7 +24,7 @@
   context "when pagination disabled" do
     before do
       resource.class_eval do
-        self.default_page_size = 2
+        self.page_default_size = 2
       end
     end
 
diff --git a/spec/performance/chart_page.rb b/spec/performance/chart_page.rb
new file mode 100644
index 00000000..6da99501
--- /dev/null
+++ b/spec/performance/chart_page.rb
@@ -0,0 +1,683 @@
+require "json"
+require "fileutils"
+require_relative "scenarios"
+# Guarded because measure_releases may already be running as the main script.
+require_relative "measure_releases" unless defined?(MeasureReleases)
+
+# Renders performance_history.tsv to tmp/performance.html and opens it.
+module ChartPage
+  ROOT = File.expand_path("../..", __dir__)
+  HISTORY = File.expand_path("performance_history.tsv", __dir__)
+  OUTPUT = File.join(ROOT, "tmp", "performance.html")
+  WORKING_TREE = "working tree"
+  CHART_LIBRARY = File.expand_path("vendor/chart.umd.js", __dir__)
+
+  module_function
+
+  # measured skips the probe when the caller already has the numbers.
+  def run(open: true, current: true, measured: nil)
+    abort "no #{HISTORY}, run rake performance:record_all" unless File.exist?(HISTORY)
+
+    rows = measurements
+    rows += measured || working_tree if current
+    FileUtils.mkdir_p(File.dirname(OUTPUT))
+    File.write(OUTPUT, page(rows))
+    launch if open
+    OUTPUT
+  end
+
+  def as_rows(measured)
+    measured.map do |_, ruby, mode, scenario, phase, allocations, milliseconds|
+      row(WORKING_TREE, ruby, mode, scenario, phase, allocations, milliseconds, MeasureReleases.machine)
+    end
+  end
+
+  def row(version, ruby, concurrency, scenario, phase, allocations, milliseconds, machine)
+    {
+      version: version, ruby: ruby, concurrency: concurrency,
+      scenario: scenario, phase: phase, count: allocations.to_i,
+      ms: milliseconds.to_s.empty? ? nil : milliseconds.to_f,
+      machine: machine.to_s
+    }
+  end
+
+  # Plotted as one more release so the branch you are on sits beside the history.
+  def working_tree
+    MeasureReleases::Progress.start(MeasureReleases::MODES.length * MeasureReleases.measurements_per_mode)
+    MeasureReleases::MODES.flat_map { |mode|
+      measured = MeasureReleases.measure_in(MeasureReleases::ROOT, mode) || []
+      measured.map { |scenario, phase, allocations, milliseconds|
+        row(WORKING_TREE, MeasureReleases::RUBY, mode, scenario, phase, allocations, milliseconds, MeasureReleases.machine)
+      }
+    }
+  ensure
+    MeasureReleases::Progress.finish
+  end
+
+  def measurements
+    File.readlines(HISTORY, chomp: true).drop(1).reject(&:empty?).map do |line|
+      cells = line.split("\t").first(8)
+      row(*cells.fill(nil, cells.length...8))
+    end
+  end
+
+  def launch
+    opener = if RUBY_PLATFORM.include?("darwin")
+      "open"
+    else
+      (system("which xdg-open > /dev/null 2>&1") ? "xdg-open" : nil)
+    end
+    opener ? system(opener, OUTPUT) : puts("open it yourself, no opener found")
+  end
+
+  def page(rows)
+    # Block form throughout: a replacement string would read \\1 and friends in
+    # minified javascript and json as backreferences.
+    TEMPLATE
+      .sub("__DATA__") { JSON.generate(rows) }
+      .sub("__NAMES__") { JSON.generate(Scenarios.descriptions) }
+      .sub("__WAITING__") { JSON.generate(Scenarios.waiting) }
+      .gsub("__NOISE__") { JSON.generate(NOISE_FLOOR) }
+      .sub("__CHARTJS__") { File.read(CHART_LIBRARY) }
+  end
+
+  # Six repeats of the probe, worst measurement in each group rounded up: 1.7% where nothing waits, 3.7% where something does.
+  NOISE_FLOOR = {quiet: 0.02, waiting: 0.04}.freeze
+
+  TEMPLATE = <<~HTML
+    
+    
+    
+    graphiti performance
+    
+

graphiti performance

+
+
+ + + + + +
+
+
+
+
+
+
+
+
+ + +
+
+ + + + + + +
releaseconcurrencyrubyvaluechangesince
+
+
+
+ + + HTML +end + +if $PROGRAM_NAME == __FILE__ + path = ChartPage.run(open: !ARGV.include?("--no-open"), current: !ARGV.include?("--no-current")) + puts "wrote #{path}" +end diff --git a/spec/performance/measure_release.rb b/spec/performance/measure_release.rb new file mode 100644 index 00000000..676ae10b --- /dev/null +++ b/spec/performance/measure_release.rb @@ -0,0 +1,136 @@ +# Runs inside a checkout of one released tag, so it can only use the public API. +$LOAD_PATH.unshift(File.expand_path("lib", Dir.pwd)) +# Old versions use these without requiring them, which was fine when they were +# default gems and loaded for free. +require "ostruct" +require "bigdecimal" +require "active_model" +require "graphiti" +if Graphiti::Resource.respond_to?(:relationship_links=) + Graphiti::Resource.relationship_links = false +elsif Graphiti::Resource.respond_to?(:autolink=) + Graphiti::Resource.autolink = false +end +require File.expand_path("spec/fixtures/poro.rb", Dir.pwd) +Graphiti.setup! + +require File.expand_path("scenarios.rb", __dir__) + +LATENCY_OVERRIDE = ENV["PERF_LATENCY"] && Float(ENV["PERF_LATENCY"]) + +module QueryLatency + class << self + attr_accessor :seconds + end + self.seconds = 0.0 + + def self.for(scenario) + LATENCY_OVERRIDE || scenario[:latency].to_f + end +end + +# Left unprepended when nothing waits, so a scenario measuring graphiti's own work does not pay for the wrapper. +if Scenarios::ALL.each_value.any? { |scenario| QueryLatency.for(scenario) > 0 } && defined?(PORO::Adapter) + PORO::Adapter.prepend(Module.new do + def resolve(scope) + sleep(QueryLatency.seconds) if QueryLatency.seconds > 0 + super + end + end) +end + +module Scenarios + SCENARIOS = ::Scenarios::ALL + + PHASE_CALLS = { + "resolve" => ->(proxy) { proxy.to_a }, + "render" => ->(proxy) { proxy.to_jsonapi } + }.freeze + + MODES = %w[off on].freeze + # A budget rather than a pass count, so cheap and expensive scenarios both get enough passes. + TIMING_BUDGET_SECONDS = 1.0 + MINIMUM_TIMED_PASSES = 5 + + module_function + + def seed(employees:, positions_per_employee: 0, departments: 1, credit_cards_per_employee: 0) + department_ids = Array.new(departments) { |index| PORO::Department.create(name: "d#{index}").id } + + employees.times do |employee_index| + employee = PORO::Employee.create(first_name: "f#{employee_index}", age: 30) + positions_per_employee.times do |position_index| + PORO::Position.create( + employee_id: employee.id, + title: "t#{position_index}", + department_id: department_ids[position_index % department_ids.length] + ) + end + credit_cards_per_employee.times do |card_index| + PORO::Visa.create(employee_id: employee.id, number: card_index) + end + end + end + + def allocations + GC.start(full_mark: true, immediate_sweep: true) + GC.disable + before = GC.stat[:total_allocated_objects] + yield + GC.stat[:total_allocated_objects] - before + ensure + GC.enable + end + + # Old versions mutate the params they are given, and warm-up passes settle memoized state. + def measure(scenario, phase, context) + call = PHASE_CALLS.fetch(phase) + + QueryLatency.seconds = QueryLatency.for(scenario) + PORO::DB.clear + seed(**scenario[:seed]) + + fresh_params = -> { Marshal.load(Marshal.dump(scenario[:params])) } + run = -> { call.call(PORO::EmployeeResource.all(fresh_params.call)) } + + Graphiti.with_context(context, :index) do + 2.times { run.call } + objects = allocations(&run) + [objects, (fastest(&run) * 1000).round(3)] + end + end + + def fastest + fastest = Float::INFINITY + passes = 0 + deadline = now + TIMING_BUDGET_SECONDS + while passes < MINIMUM_TIMED_PASSES || now < deadline + started = now + yield + fastest = [fastest, now - started].min + passes += 1 + end + fastest + end + + def now + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + + def each_measurement(context) + SCENARIOS.each_pair do |id, scenario| + ::Scenarios::PHASES.each do |phase| + puts "START\t#{id}\t#{phase}" + yield id, phase, *measure(scenario, phase, context) + end + end + end +end + +$stdout.sync = true +Graphiti.config.concurrency = (ARGV[0] == "on") +CONTEXT = Struct.new(:current_user).new("none") + +Scenarios.each_measurement(CONTEXT) do |scenario, phase, count, milliseconds| + puts "ROW\t#{scenario}\t#{phase}\t#{count}\t#{milliseconds}" +end diff --git a/spec/performance/measure_releases.rb b/spec/performance/measure_releases.rb new file mode 100644 index 00000000..fb74afbf --- /dev/null +++ b/spec/performance/measure_releases.rb @@ -0,0 +1,614 @@ +require "bundler" +require "io/console" +require "fileutils" +require "open3" +require "tmpdir" +require "digest" +require "etc" +require_relative "scenarios" + +# Fills performance_history.tsv. rake -T performance documents the tasks. The +# remaining --promote is called by semantic-release and is not one of them. +module MeasureReleases + ROOT = File.expand_path("../..", __dir__) + MEASURE = File.expand_path("measure_release.rb", __dir__) + HISTORY = File.expand_path("performance_history.tsv", __dir__) + WORKTREE = File.join(Dir.tmpdir, "graphiti-measure-releases") + # The first release that boots on ruby 3. Everything older passes a hash where a + # keyword argument is expected, in graphiti's own code and in the dry-types it pins. + OLDEST = "v1.2.34" + + COLUMNS = %w[version ruby concurrency scenario phase allocations milliseconds machine tree scenarios].freeze + PENDING = "pending" + + ALLOCATIONS = COLUMNS.index("allocations") + MILLISECONDS = COLUMNS.index("milliseconds") + MACHINE = COLUMNS.index("machine") + TREE = COLUMNS.index("tree") + SCENARIOS = COLUMNS.index("scenarios") + # Minor, because a patch release of ruby does not move allocations + RUBY = RUBY_VERSION.split(".").first(2).join(".") + + # Ruby 3.4 unbundled these, and tags older than that never named them. + SHIMS = %w[ostruct bigdecimal mutex_m base64 logger].freeze + + # Skips the test group, whose native extensions (byebug) don't build everywhere the probe runs. + WITHOUT_TEST = {"BUNDLE_WITHOUT" => "test"}.freeze + + # One line on a tty, nothing at all when the output is piped or captured. + module Progress + BAR_WIDTH = 24 + + module_function + + # A line wider than the terminal wraps, and the carriage return then only + # reaches the start of the last fragment, so every draw appends a line. + def line_width + @line_width ||= begin + _, columns = $stderr.winsize + [columns - 1, 40].max + rescue + 78 + end + end + + def start(total, prefix = nil) + @total = total + @prefix = prefix + @done = 0 + @drawing = $stderr.tty? + end + + def step(label) + @done += 1 + draw(label) + end + + # Redraws without advancing, for the checkout and install between measurements. + def waiting(label) + draw(label) + end + + def draw(label) + return unless @drawing + + filled = BAR_WIDTH * @done / @total + bar = ("=" * filled).ljust(BAR_WIDTH, ".") + line = "[#{bar}] #{@done}/#{@total} #{[@prefix, label].compact.join(" ")}" + $stderr.print("\r" + line.ljust(line_width)[0, line_width]) + end + + def finish + return unless @drawing + + $stderr.print("\r" + (" " * line_width) + "\r") + @drawing = false + end + end + + module_function + + # Timings only compare within one box, so the history records which one took them. + def machine + @machine ||= begin + brand = if RUBY_PLATFORM.include?("darwin") + `sysctl -n machdep.cpu.brand_string 2>/dev/null` + elsif File.exist?("/proc/cpuinfo") + File.readlines("/proc/cpuinfo").grep(/model name/).first.to_s.split(":").last + end + "#{brand.to_s.strip.tr("\t", " ")}/#{Etc.nprocessors}" + end + end + + def timed_elsewhere + recorded + .select { |row| row[1] == RUBY && !row[MILLISECONDS].to_s.empty? } + .map { |row| row[MACHINE] } + .uniq - [machine, ""] + end + + def forced? + ARGV.include?("--force") + end + + def assert_reference_cpu! + return if forced? + + others = timed_elsewhere + return if others.empty? + + abort "the reference CPU for the ruby #{RUBY} timings is #{others.join(", ")} and this is #{machine}. " \ + "Pass --force to make this the reference, or rake performance:read to read the change." + end + + def stamp(rows, tree = measured_tree) + rows.map { |row| row.fill("", row.length...MACHINE) + [machine, tree, scenario_fingerprint] } + end + + # What was measured rather than the file that describes it, so editing a + # description or a comment does not invalidate a series. + def scenario_fingerprint + @scenario_fingerprint ||= begin + measured = Scenarios::ALL.transform_values { |scenario| scenario.slice(:seed, :params, :latency) } + Digest::SHA256.hexdigest(measured.inspect)[0, 12] + end + end + + def measured_with + recorded + .select { |row| row[1] == RUBY && !row[MILLISECONDS].to_s.empty? } + .map { |row| row[SCENARIOS] } + .uniq - [scenario_fingerprint, ""] + end + + def assert_same_scenarios! + others = measured_with + return if others.empty? || forced? + + abort "the ruby #{RUBY} rows measured scenarios #{others.join(", ")} and these are #{scenario_fingerprint}. " \ + "The series is not comparable across a change to what is measured, so re-record it with " \ + "rake performance:record ALL=1, or pass --force to record beside them anyway." + end + + MEASURED_BY = ["lib", "spec/fixtures/poro.rb"].freeze + PROBED_BY = ["spec/performance/measure_release.rb", "spec/performance/scenarios.rb"].freeze + NOT_MEASURED_BY = ["lib/graphiti/version.rb"].freeze + + def measured_tree(directory = ROOT) + tree_fingerprints[directory] ||= begin + measured = tracked(directory, MEASURED_BY) + abort "nothing to fingerprint in #{directory}, so a measurement of it could not be told apart" if measured.empty? + + sources = measured.map { |file| [file, File.join(directory, file)] } + + tracked(ROOT, PROBED_BY).map { |file| [file, File.join(ROOT, file)] } + digest = sources.sort.map { |file, path| "#{file}#{File.read(path)}" }.join + Digest::SHA256.hexdigest(digest)[0, 12] + end + end + + def tree_fingerprints + @tree_fingerprints ||= {} + end + + def tracked(directory, paths) + `git -C #{directory} ls-files #{paths.join(" ")}`.lines.map(&:strip) - NOT_MEASURED_BY + end + + def measurements_per_mode + Scenarios::ALL.size * Scenarios::PHASES.size + end + + # ALL=1 replaces every row for this ruby, so there is nothing left for the new + # numbers to be incomparable with. Only a partial fill lands beside old rows. + def rebuild(only_missing: false) + assert_reference_cpu! + assert_same_scenarios! if only_missing + @skipped = [] + wanted = only_missing ? tags - recorded.select { |row| row[1] == RUBY }.map(&:first) : tags + measured = measure_sequence(wanted).flatten(1) + kept = recorded.reject { |row| row[1] == RUBY && (!only_missing || wanted.include?(row.first)) } + write(in_release_order(kept + measured)) + report_skipped(wanted.length) + ensure + remove_worktrees + end + + # A shortened series and a half-failed sweep otherwise look the same. + def report_skipped(attempted) + return puts("measured all #{attempted} releases on ruby #{RUBY}") if skipped.empty? + + puts "measured #{attempted - skipped.length} of #{attempted} releases on ruby #{RUBY}, skipped:" + skipped.each { |tag, reason| puts format(" %-16s %s", tag, reason) } + end + + def skipped + @skipped ||= [] + end + + def skip(tag, reason) + Progress.finish + puts "#{tag} skipped, #{reason}" + skipped << [tag, reason] + nil + end + + # Timed in one sitting, so they compare to each other on any machine. + def compare(count) + recent = tags.last(count) + @skipped = [] + Progress.start((recent.length + 1) * MODES.length * measurements_per_mode) + measured = measure_sequence(recent).flatten(1) + tree = MODES.flat_map { |mode| + (measure_in(ROOT, mode) || []).map { |row| ["working tree", RUBY, mode, *row] } + } + Progress.finish + print_totals(measured + tree) + report_skipped(recent.length) unless skipped.empty? + puts "\nmeasured here and not recorded, since the history belongs to one machine" + ensure + Progress.finish + remove_worktrees + end + + def print_totals(rows) + table = [%w[release concurrency objects ms]] + rows.group_by { |row| [row.first, row[2]] }.each_pair do |(version, mode), group| + table << [version, mode, group.sum { |row| row[ALLOCATIONS].to_i }.to_s, + format("%.1f", group.sum { |row| row[MILLISECONDS].to_f })] + end + + widths = table.map { |row| row.map(&:length) }.transpose.map(&:max) + table.each_with_index do |row, index| + puts row.each_with_index.map { |cell, column| + (column < 2) ? cell.ljust(widths[column]) : cell.rjust(widths[column]) + }.join(" ").rstrip + puts "-" * (widths.sum + 8) if index.zero? + end + end + + def record(tag) + abort "unknown tag #{tag}" unless tags.include?(tag) + + assert_reference_cpu! + assert_same_scenarios! + rows = measure_release(tag) + abort "#{tag} could not be measured" if rows.nil? || rows.empty? + store(tag, rows) + ensure + remove_worktrees + end + + # The release only renames these, so what ships is what was reviewed. + def record_pending + return unless series_exists? + + assert_reference_cpu! + assert_same_scenarios! + Progress.start(MODES.length * measurements_per_mode, PENDING) + rows = MODES.flat_map do |mode| + measured = measure_in(ROOT, mode) + measured ? measured.map { |row| [PENDING, RUBY, mode, *row] } : [] + end + abort "the working tree could not be measured" if rows.empty? + store(PENDING, stamp(rows)) + ensure + remove_worktrees + end + + # A row measured before two more merges describes a tree that no longer exists. + def promote(version) + pending = recorded.select { |row| row.first == PENDING } + # A gap is recoverable, a wrong number labelled as a release is not. + if pending.empty? + warn "no pending measurement, so #{version} records none. Fill it in later with rake performance:backfill" + return + end + + measured = pending.map { |row| row[TREE] }.uniq + if measured != [measured_tree] && !forced? + abort "the pending measurement is of #{measured.join(", ")} and this tree is #{measured_tree}. " \ + "Re-record it with rake performance:pending, or pass --force if the change cannot have moved it." + end + + # Stamped with what it is being released against, not what it measured, so + # the file says which tree the number is claimed to describe. + pending = pending.map { |row| row.dup.tap { |copy| copy[TREE] = measured_tree } } + warn "released a measurement of #{measured.join(", ")} as #{version}" if measured != [measured_tree] + + write(in_release_order(recorded - pending + pending.map { |row| [version, *row.drop(1)] })) + puts "released the pending measurement as #{version}" + end + + # Backfilling a new ruby here would put 31 releases of rows in a release commit. + def series_exists? + return true if recorded.any? { |row| row[1] == RUBY } + + warn "no history for ruby #{RUBY}, skipping. run rake performance:record_all to add it" + false + end + + def store(version, rows) + existing = recorded.select { |row| row.first == version && row[1] == RUBY } + report(version, existing, rows) unless existing.empty? + write(in_release_order(recorded - existing + rows)) + end + + # A tag's code cannot change, so a moved allocation count means the measurement did. + def report(tag, existing, rows) + was = existing.to_h { |row| [row[2..4], row[ALLOCATIONS].to_i] } + moved = rows.reject { |row| was[row[2..4]] == row[ALLOCATIONS].to_i } + + if moved.empty? + puts "#{tag} was already recorded on ruby #{RUBY} and measured identically" + return + end + + puts "#{tag} was already recorded on ruby #{RUBY} and #{moved.length} of #{rows.length} numbers moved:" + moved.first(10).each do |_, _, mode, scenario, phase, now, _| + before = was[[mode, scenario, phase]] + puts format(" %-46s %-7s %-3s %7s to %-7s", Scenarios.describe(scenario), phase, mode, before || "-", now) + end + puts " and #{moved.length - 10} more" if moved.length > 10 + end + + # On the reference CPU the recorded rows are comparable, so the working tree is + # measured against them. Anywhere else the releases have to be timed here too. + def read + timed_elsewhere.empty? ? current : compare(5) + end + + def current + Progress.start(MODES.length * measurements_per_mode) + rows = MODES.flat_map { |mode| measure_in(ROOT, mode)&.map { |row| ["working tree", RUBY, mode, *row] } || [] } + Progress.finish + abort "could not measure the working tree" if rows.empty? + + against = latest_release + warn "no release measured on ruby #{RUBY}" if against.empty? + print_table(against, rows) + puts " #{plot(rows)}" + ensure + Progress.finish + remove_worktrees + end + + # Hands over the measurement just taken, so the page does not repeat it. + def plot(rows) + require_relative "chart_page" + "file://#{ChartPage.run(open: false, measured: ChartPage.as_rows(rows))}" + end + + # Only refreshes a page that already exists, so recording never creates one. + def replot + require_relative "chart_page" + return unless File.exist?(ChartPage::OUTPUT) + + ChartPage.run(open: false) + puts "#{File.basename(ChartPage::OUTPUT)} refreshed" + end + + def latest_release + mine = recorded.select { |row| row[1] == RUBY && row.first != PENDING } + return [] if mine.empty? + + newest = mine.map(&:first).uniq.last + mine.select { |row| row.first == newest } + end + + def print_table(against, rows) + index = against.to_h { |row| [row[2..4], row] } + table = [%w[scenario phase concurrency objects was drift ms was]] + + rows.each do |row| + _, _, mode, scenario, phase, now, milliseconds = row + was = index[row[2..4]] + objects_was = was && was[ALLOCATIONS].to_i + table << [ + Scenarios.describe(scenario), phase, mode, + now, objects_was || "-", objects_was ? format("%+d", now.to_i - objects_was) : "-", + format("%.2f", milliseconds), milliseconds_was(was) + ] + end + + widths = table.map { |r| r.map { |c| c.to_s.length } }.transpose.map(&:max) + table.each_with_index do |r, i| + puts r.each_with_index.map { |cell, c| + (c < 3) ? cell.to_s.ljust(widths[c]) : cell.to_s.rjust(widths[c]) + }.join(" ").rstrip + puts "-" * (widths.sum + 10) if i.zero? + end + puts "\nagainst #{against.first&.first} on ruby #{RUBY}" unless against.empty? + puts "\nplotted beside the release history:" + end + + def milliseconds_was(row) + return "-" if row.nil? || row[MILLISECONDS].to_s.empty? + + format("%.2f", row[MILLISECONDS].to_f) + end + + def write(rows) + content = ([COLUMNS] + rows).map { |row| row.join("\t") }.join("\n") + "\n" + name = File.basename(HISTORY) + if File.exist?(HISTORY) && File.read(HISTORY) == content + puts "#{name} wasn't modified" + return + end + + File.write(HISTORY, content) + puts "#{name} updated" + replot + end + + # Rows recorded before timings were kept have no milliseconds cell. + def recorded + return [] unless File.exist?(HISTORY) + + File.readlines(HISTORY, chomp: true).drop(1).reject(&:empty?).map do |line| + line.split("\t").fill("", line.count("\t") + 1...COLUMNS.length) + end + end + + def tags + all = `git -C #{ROOT} tag --list "v*"`.split("\n") + sorted = all.sort_by { |tag| Gem::Version.new(tag.delete_prefix("v")) } + sorted.drop_while { |tag| tag != OLDEST } + rescue ArgumentError + abort "could not order tags by version" + end + + # Total, so identical measurements always produce an identical file. + def in_release_order(rows) + order = tags + scenarios = Scenarios::ALL.keys + rows.sort_by do |row| + [ + (row.first == PENDING) ? order.length + 1 : order.index(row.first) || order.length, + ruby_sort_key(row[1]), + (row[2] == "off") ? 0 : 1, + scenarios.index(row[3]) || scenarios.length, + (row[4] == "resolve") ? 0 : 1 + ] + end + end + + def ruby_sort_key(version) + Gem::Version.new(version) + rescue ArgumentError + Gem::Version.new("0") + end + + # Roughly one release in six lands in a slow window and reads high across every + # scenario at once, which is why a jump is judged on the total rather than a + # single measurement. + RETRY_JUMP = 0.10 + + def measure_sequence(wanted) + previous_tag = nil + previous = nil + + puts "measuring #{wanted.length} releases on ruby #{RUBY}, #{MODES.join(" and ")} concurrency, " \ + "#{measurements_per_mode} measurements each" + + wanted.each_with_index.filter_map do |tag, index| + counted = "#{index + 1}/#{wanted.length} #{tag}" + rows = measure_release(tag, counted) + next if rows.nil? + + jump = jump_from(previous, rows) + if jump + puts format("%s totalled %+.0f%% against %s, measuring it again", tag, jump * 100, previous_tag) + rows = fastest_of(rows, measure_release(tag, "#{counted} again")) + end + previous_tag = tag + previous = rows + rows + end + end + + def jump_from(previous, rows) + return nil if previous.nil? + + jumps = MODES.filter_map do |mode| + was = total_milliseconds(previous, mode) + next unless was.positive? + + (total_milliseconds(rows, mode) - was) / was + end + jumps.max&.then { |jump| jump if jump > RETRY_JUMP } + end + + def total_milliseconds(rows, mode) + rows.sum { |row| (row[2] == mode) ? row[MILLISECONDS].to_f : 0.0 } + end + + # Noise only ever reads slow, so the faster pass is the honest one. Allocations + # are deterministic and identical in both, so the whole row can come from either. + def fastest_of(rows, again) + return rows if again.nil? + + best = again.to_h { |row| [row[2..4], row] } + rows.map do |row| + other = best[row[2..4]] + (other && other[MILLISECONDS].to_f < row[MILLISECONDS].to_f) ? other : row + end + end + + def measure_release(tag, prefix = tag) + Progress.start(MODES.length * measurements_per_mode, prefix) + Progress.waiting("checking out") + checkout(tag) + Progress.waiting("bundle install") + return skip(tag, "bundle install failed") unless bundled? + + tree = measured_tree(WORKTREE) + measured = MODES.map { |mode| [mode, measure_in(WORKTREE, mode)] } + empty = measured.find { |_, rows| rows.nil? } + return skip(tag, failure(empty.first)) if empty + + stamp(measured.flat_map { |mode, rows| rows.map { |row| [tag, RUBY, mode, *row] } }, tree) + ensure + Progress.finish + end + + # The child's first error line is what says why a release will not boot. + def failure(mode) + output, _ = Bundler.with_unbundled_env do + Open3.capture2e(WITHOUT_TEST, "bundle", "exec", "ruby", MEASURE, mode, chdir: WORKTREE) + end + line = output.lines.find { |l| !l.include?("warning:") && !l.start_with?("\t", " ") } + return "measurement failed" unless line + + line.strip.sub("#{WORKTREE}/", "").sub(/\A\S+:\d+:in '[^']*': /, "")[0, 80] + end + + def checkout(tag, directory = WORKTREE) + remove_worktree(directory) + system("git", "-C", ROOT, "worktree", "add", "--detach", directory, tag, out: File::NULL, err: File::NULL) + File.open(File.join(directory, "Gemfile"), "a") do |file| + SHIMS.each { |gem_name| file.puts(%(gem "#{gem_name}")) } + end + allow_current_ruby(directory) + end + + # Tags through v1.2.34 cap required_ruby_version below rubies that can still run them. + def allow_current_ruby(directory) + gemspec = File.join(directory, "graphiti.gemspec") + lines = File.readlines(gemspec).reject { |line| line.include?("required_ruby_version") } + File.write(gemspec, lines.join) + end + + def bundled?(directory = WORKTREE) + Bundler.with_unbundled_env do + system(WITHOUT_TEST, "bundle", "install", chdir: directory, out: File::NULL, err: File::NULL) + end + end + + # The child would otherwise inherit BUNDLE_GEMFILE and resolve against the working tree. + def measure_in(directory, mode) + environment = WITHOUT_TEST + rows = [] + status = nil + + Bundler.with_unbundled_env do + Open3.popen2e(environment, "bundle", "exec", "ruby", MEASURE, mode, chdir: directory) do |input, output, thread| + input.close + output.each_line do |line| + fields = line.chomp.split("\t") + case fields.first + when "START" then Progress.step("concurrency #{mode} #{fields[2]} #{Scenarios.describe(fields[1])}") + when "ROW" then rows << fields.drop(1) + end + end + status = thread.value + end + end + return nil unless status.success? + + rows + end + + def remove_worktrees + remove_worktree(WORKTREE) + end + + def remove_worktree(directory) + system("git", "-C", ROOT, "worktree", "remove", "--force", directory, out: File::NULL, err: File::NULL) + FileUtils.rm_rf(directory) + end + + MODES = %w[off on].freeze +end + +if $PROGRAM_NAME == __FILE__ + case ARGV.first + when "--read" then MeasureReleases.read + when "--missing" then MeasureReleases.rebuild(only_missing: true) + when "--all" then MeasureReleases.rebuild + when "--pending" then MeasureReleases.record_pending + when "--promote" + abort "--promote needs a version, e.g. --promote v2.0.0-beta.11" unless ARGV[1] + MeasureReleases.promote(ARGV[1]) + else MeasureReleases.record(ARGV.first) + end +end diff --git a/spec/performance/performance_history.tsv b/spec/performance/performance_history.tsv new file mode 100644 index 00000000..a24abc31 --- /dev/null +++ b/spec/performance/performance_history.tsv @@ -0,0 +1,2441 @@ +version ruby concurrency scenario phase allocations milliseconds machine tree scenarios +v1.2.34 4.0 off flat_10 resolve 283 0.055 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off flat_10 render 2001 0.298 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off flat_100 resolve 1182 0.182 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off flat_100 render 17841 2.469 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off sparse_100 resolve 1200 0.194 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off sparse_100 render 16461 2.148 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off include_1 resolve 3801 0.768 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off include_1 render 26522 4.033 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off include_2 resolve 4345 0.994 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off include_2 render 35408 5.663 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off include_3 resolve 7077 1.499 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off include_3 render 47172 7.376 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off siblings_50 resolve 5839 18.237 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off siblings_50 render 36521 24.026 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off chain_and_sibling_50 resolve 6382 23.761 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off chain_and_sibling_50 render 45406 32.023 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off deep_and_sibling_50 resolve 9114 31.238 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off deep_and_sibling_50 render 57169 39.873 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off stats_100 resolve 1201 0.186 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 off stats_100 render 17864 2.394 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on flat_10 resolve 283 0.055 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on flat_10 render 2001 0.305 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on flat_100 resolve 1182 0.192 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on flat_100 render 17841 2.549 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on sparse_100 resolve 1200 0.189 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on sparse_100 render 16461 2.047 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on include_1 resolve 3844 10.439 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on include_1 render 26565 14.342 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on include_2 resolve 4431 12.746 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on include_2 render 35494 19.632 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on include_3 resolve 7206 23.211 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on include_3 render 47301 31.082 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on siblings_50 resolve 5925 16.229 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on siblings_50 render 36607 22.042 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on chain_and_sibling_50 resolve 6511 25.638 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on chain_and_sibling_50 render 45535 35.133 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on deep_and_sibling_50 resolve 9286 37.663 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on deep_and_sibling_50 render 57341 48.135 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on stats_100 resolve 1201 0.188 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.34 4.0 on stats_100 render 17864 2.63 Apple M1 Max/10 42640aaf9400 2b9685be19cf +v1.2.35 4.0 off flat_10 resolve 253 0.053 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off flat_10 render 1974 0.304 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off flat_100 resolve 1152 0.182 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off flat_100 render 17814 2.462 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off sparse_100 resolve 1170 0.185 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off sparse_100 render 16434 2.061 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off include_1 resolve 3741 0.752 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off include_1 render 26465 4.081 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off include_2 resolve 4255 0.97 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off include_2 render 35321 5.617 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off include_3 resolve 6957 1.49 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off include_3 render 47055 7.522 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off siblings_50 resolve 5749 17.927 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off siblings_50 render 36434 23.746 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off chain_and_sibling_50 resolve 6262 22.709 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off chain_and_sibling_50 render 45289 31.754 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off deep_and_sibling_50 resolve 8964 29.735 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off deep_and_sibling_50 render 57022 39.269 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off stats_100 resolve 1171 0.189 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 off stats_100 render 17837 2.423 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on flat_10 resolve 253 0.051 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on flat_10 render 1974 0.295 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on flat_100 resolve 1152 0.178 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on flat_100 render 17814 2.433 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on sparse_100 resolve 1170 0.183 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on sparse_100 render 16434 2.113 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on include_1 resolve 3784 10.282 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on include_1 render 26508 14.566 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on include_2 resolve 4341 12.947 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on include_2 render 35407 16.984 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on include_3 resolve 7086 23.45 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on include_3 render 47184 19.638 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on siblings_50 resolve 5835 15.685 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on siblings_50 render 36520 21.381 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on chain_and_sibling_50 resolve 6391 25.963 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on chain_and_sibling_50 render 45418 34.264 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on deep_and_sibling_50 resolve 9136 37.81 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on deep_and_sibling_50 render 57194 47.089 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on stats_100 resolve 1171 0.182 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.35 4.0 on stats_100 render 17837 2.412 Apple M1 Max/10 5788df6cdc59 2b9685be19cf +v1.2.38 4.0 off flat_10 resolve 253 0.052 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off flat_10 render 1974 0.297 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off flat_100 resolve 1152 0.179 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off flat_100 render 17814 2.444 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off sparse_100 resolve 1170 0.183 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off sparse_100 render 16434 2.014 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off include_1 resolve 3741 0.755 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off include_1 render 26465 3.986 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off include_2 resolve 4255 0.974 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off include_2 render 35321 5.856 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off include_3 resolve 6957 1.576 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off include_3 render 47055 8.02 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off siblings_50 resolve 5749 17.814 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off siblings_50 render 36434 24.066 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off chain_and_sibling_50 resolve 6262 23.353 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off chain_and_sibling_50 render 45289 32.569 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off deep_and_sibling_50 resolve 8964 30.962 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off deep_and_sibling_50 render 57022 39.902 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off stats_100 resolve 1171 0.183 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 off stats_100 render 17837 2.43 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on flat_10 resolve 253 0.052 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on flat_10 render 1974 0.296 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on flat_100 resolve 1152 0.18 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on flat_100 render 17814 2.423 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on sparse_100 resolve 1170 0.184 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on sparse_100 render 16434 2.012 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on include_1 resolve 3784 10.333 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on include_1 render 26508 14.914 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on include_2 resolve 4341 12.445 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on include_2 render 35407 17.396 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on include_3 resolve 7086 21.7 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on include_3 render 47184 19.651 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on siblings_50 resolve 5835 15.471 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on siblings_50 render 36520 21.392 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on chain_and_sibling_50 resolve 6391 28.033 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on chain_and_sibling_50 render 45418 35.15 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on deep_and_sibling_50 resolve 9136 39.905 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on deep_and_sibling_50 render 57194 49.774 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on stats_100 resolve 1171 0.185 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.38 4.0 on stats_100 render 17837 2.415 Apple M1 Max/10 bb99fa8bada6 2b9685be19cf +v1.2.39 4.0 off flat_10 resolve 253 0.051 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off flat_10 render 1974 0.296 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off flat_100 resolve 1152 0.179 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off flat_100 render 17814 2.444 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off sparse_100 resolve 1170 0.19 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off sparse_100 render 16434 2.223 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off include_1 resolve 3741 0.776 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off include_1 render 26465 4.228 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off include_2 resolve 4255 0.988 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off include_2 render 35321 5.744 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off include_3 resolve 6957 1.469 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off include_3 render 47055 7.582 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off siblings_50 resolve 5749 17.587 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off siblings_50 render 36434 23.366 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off chain_and_sibling_50 resolve 6262 26.347 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off chain_and_sibling_50 render 45289 32.974 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off deep_and_sibling_50 resolve 8964 32.428 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off deep_and_sibling_50 render 57022 40.15 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off stats_100 resolve 1171 0.184 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 off stats_100 render 17837 2.409 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on flat_10 resolve 253 0.052 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on flat_10 render 1974 0.3 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on flat_100 resolve 1152 0.185 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on flat_100 render 17814 2.607 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on sparse_100 resolve 1170 0.189 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on sparse_100 render 16434 2.079 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on include_1 resolve 3784 10.457 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on include_1 render 26508 14.802 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on include_2 resolve 4341 12.21 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on include_2 render 35407 18.761 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on include_3 resolve 7086 22.082 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on include_3 render 47184 30.479 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on siblings_50 resolve 5835 16.125 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on siblings_50 render 36520 23.381 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on chain_and_sibling_50 resolve 6391 26.739 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on chain_and_sibling_50 render 45418 34.445 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on deep_and_sibling_50 resolve 9136 39.63 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on deep_and_sibling_50 render 57194 50.664 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on stats_100 resolve 1171 0.184 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.39 4.0 on stats_100 render 17837 2.436 Apple M1 Max/10 038099a48c2c 2b9685be19cf +v1.2.40 4.0 off flat_10 resolve 253 0.054 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off flat_10 render 1974 0.3 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off flat_100 resolve 1152 0.183 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off flat_100 render 17814 2.419 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off sparse_100 resolve 1170 0.185 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off sparse_100 render 16434 2.038 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off include_1 resolve 3741 0.744 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off include_1 render 26465 3.977 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off include_2 resolve 4255 0.97 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off include_2 render 35321 5.625 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off include_3 resolve 6957 1.495 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off include_3 render 47055 7.4 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off siblings_50 resolve 5749 17.888 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off siblings_50 render 36434 24.827 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off chain_and_sibling_50 resolve 6262 24.487 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off chain_and_sibling_50 render 45289 31.553 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off deep_and_sibling_50 resolve 8964 29.856 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off deep_and_sibling_50 render 57022 40.09 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off stats_100 resolve 1171 0.189 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 off stats_100 render 17837 2.436 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on flat_10 resolve 253 0.052 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on flat_10 render 1974 0.302 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on flat_100 resolve 1152 0.181 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on flat_100 render 17814 2.419 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on sparse_100 resolve 1170 0.185 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on sparse_100 render 16434 2.06 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on include_1 resolve 3784 10.368 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on include_1 render 26508 14.499 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on include_2 resolve 4341 13.574 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on include_2 render 35407 18.473 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on include_3 resolve 7086 22.224 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on include_3 render 47184 29.547 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on siblings_50 resolve 5835 16.754 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on siblings_50 render 36520 22.873 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on chain_and_sibling_50 resolve 6391 26.965 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on chain_and_sibling_50 render 45418 34.949 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on deep_and_sibling_50 resolve 9136 37.094 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on deep_and_sibling_50 render 57194 48.247 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on stats_100 resolve 1171 0.188 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.40 4.0 on stats_100 render 17837 2.413 Apple M1 Max/10 cd98c5071ffa 2b9685be19cf +v1.2.41 4.0 off flat_10 resolve 253 0.052 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off flat_10 render 1974 0.301 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off flat_100 resolve 1152 0.183 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off flat_100 render 17814 2.462 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off sparse_100 resolve 1170 0.188 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off sparse_100 render 16434 2.074 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off include_1 resolve 3741 0.764 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off include_1 render 26465 4.034 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off include_2 resolve 4255 0.983 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off include_2 render 35321 5.647 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off include_3 resolve 6957 1.461 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off include_3 render 47055 8.185 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off siblings_50 resolve 5749 17.399 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off siblings_50 render 36434 24.022 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off chain_and_sibling_50 resolve 6262 23.588 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off chain_and_sibling_50 render 45289 32.526 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off deep_and_sibling_50 resolve 8964 30.621 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off deep_and_sibling_50 render 57022 39.26 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off stats_100 resolve 1171 0.184 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 off stats_100 render 17837 2.453 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on flat_10 resolve 253 0.052 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on flat_10 render 1974 0.299 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on flat_100 resolve 1152 0.181 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on flat_100 render 17814 2.451 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on sparse_100 resolve 1170 0.183 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on sparse_100 render 16434 2.044 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on include_1 resolve 3784 10.253 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on include_1 render 26508 14.482 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on include_2 resolve 4341 12.785 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on include_2 render 35407 18.212 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on include_3 resolve 7086 21.426 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on include_3 render 47184 20.516 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on siblings_50 resolve 5835 16.132 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on siblings_50 render 36520 22.233 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on chain_and_sibling_50 resolve 6391 26.851 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on chain_and_sibling_50 render 45418 33.709 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on deep_and_sibling_50 resolve 9136 40.751 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on deep_and_sibling_50 render 57194 48.377 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on stats_100 resolve 1171 0.184 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.41 4.0 on stats_100 render 17837 2.368 Apple M1 Max/10 86d3de7d4b03 2b9685be19cf +v1.2.43 4.0 off flat_10 resolve 253 0.052 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off flat_10 render 1974 0.299 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off flat_100 resolve 1152 0.18 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off flat_100 render 17814 2.438 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off sparse_100 resolve 1170 0.185 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off sparse_100 render 16434 2.049 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off include_1 resolve 3741 0.769 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off include_1 render 26465 4.366 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off include_2 resolve 4255 1.023 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off include_2 render 35321 5.686 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off include_3 resolve 6957 1.491 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off include_3 render 47055 7.623 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off siblings_50 resolve 5749 17.937 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off siblings_50 render 36434 25.162 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off chain_and_sibling_50 resolve 6262 23.948 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off chain_and_sibling_50 render 45289 31.269 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off deep_and_sibling_50 resolve 8964 31.547 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off deep_and_sibling_50 render 57022 40.611 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off stats_100 resolve 1171 0.184 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 off stats_100 render 17837 2.425 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on flat_10 resolve 253 0.052 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on flat_10 render 1974 0.298 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on flat_100 resolve 1152 0.181 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on flat_100 render 17814 2.484 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on sparse_100 resolve 1170 0.189 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on sparse_100 render 16434 2.17 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on include_1 resolve 3784 10.46 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on include_1 render 26508 14.683 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on include_2 resolve 4341 11.572 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on include_2 render 35407 17.61 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on include_3 resolve 7086 22.519 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on include_3 render 47184 18.779 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on siblings_50 resolve 5835 15.772 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on siblings_50 render 36520 22.345 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on chain_and_sibling_50 resolve 6391 26.723 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on chain_and_sibling_50 render 45418 36.278 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on deep_and_sibling_50 resolve 9136 38.267 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on deep_and_sibling_50 render 57194 50.237 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on stats_100 resolve 1171 0.185 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.43 4.0 on stats_100 render 17837 2.377 Apple M1 Max/10 af0dc0a9a79d 2b9685be19cf +v1.2.44 4.0 off flat_10 resolve 253 0.054 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off flat_10 render 1974 0.316 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off flat_100 resolve 1152 0.184 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off flat_100 render 17814 2.505 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off sparse_100 resolve 1170 0.188 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off sparse_100 render 16434 2.053 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off include_1 resolve 3741 0.762 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off include_1 render 26465 4.076 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off include_2 resolve 4255 0.979 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off include_2 render 35321 5.792 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off include_3 resolve 6957 1.467 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off include_3 render 47055 7.651 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off siblings_50 resolve 5749 17.971 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off siblings_50 render 36434 25.281 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off chain_and_sibling_50 resolve 6262 23.463 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off chain_and_sibling_50 render 45289 32.736 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off deep_and_sibling_50 resolve 8964 31.891 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off deep_and_sibling_50 render 57022 39.623 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off stats_100 resolve 1171 0.183 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 off stats_100 render 17837 2.562 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on flat_10 resolve 253 0.054 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on flat_10 render 1974 0.301 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on flat_100 resolve 1152 0.182 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on flat_100 render 17814 2.452 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on sparse_100 resolve 1170 0.185 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on sparse_100 render 16434 2.055 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on include_1 resolve 3784 10.661 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on include_1 render 26508 13.902 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on include_2 resolve 4341 12.842 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on include_2 render 35407 19.749 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on include_3 resolve 7086 23.396 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on include_3 render 47184 31.005 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on siblings_50 resolve 5835 15.929 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on siblings_50 render 36520 23.058 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on chain_and_sibling_50 resolve 6391 27.473 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on chain_and_sibling_50 render 45418 35.281 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on deep_and_sibling_50 resolve 9136 38.435 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on deep_and_sibling_50 render 57194 49.427 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on stats_100 resolve 1171 0.195 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.2.44 4.0 on stats_100 render 17837 2.42 Apple M1 Max/10 fb3a12ee3fff 2b9685be19cf +v1.3.6 4.0 off flat_10 resolve 254 0.055 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off flat_10 render 1995 0.308 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off flat_100 resolve 1153 0.192 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off flat_100 render 18015 2.473 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off sparse_100 resolve 1171 0.196 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off sparse_100 render 16635 2.099 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off include_1 resolve 3743 0.784 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off include_1 render 26967 4.141 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off include_2 resolve 4258 1.005 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off include_2 render 36224 5.969 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off include_3 resolve 6961 1.534 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off include_3 render 48359 7.899 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off siblings_50 resolve 5752 21.069 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off siblings_50 render 37137 24.533 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off chain_and_sibling_50 resolve 6266 25.624 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off chain_and_sibling_50 render 46393 33.137 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off deep_and_sibling_50 resolve 8969 30.831 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off deep_and_sibling_50 render 58527 42.372 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off stats_100 resolve 1172 0.197 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 off stats_100 render 18038 2.473 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on flat_10 resolve 254 0.054 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on flat_10 render 1995 0.306 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on flat_100 resolve 1153 0.187 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on flat_100 render 18015 2.478 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on sparse_100 resolve 1171 0.192 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on sparse_100 render 16635 2.095 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on include_1 resolve 3786 10.62 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on include_1 render 27010 15.496 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on include_2 resolve 4344 12.928 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on include_2 render 36310 18.359 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on include_3 resolve 7090 25.171 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on include_3 render 48488 21.6 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on siblings_50 resolve 5838 15.746 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on siblings_50 render 37223 22.199 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on chain_and_sibling_50 resolve 6395 26.34 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on chain_and_sibling_50 render 46522 38.644 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on deep_and_sibling_50 resolve 9141 39.324 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on deep_and_sibling_50 render 58699 51.968 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on stats_100 resolve 1172 0.192 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.6 4.0 on stats_100 render 18038 2.449 Apple M1 Max/10 270624e169e0 2b9685be19cf +v1.3.9 4.0 off flat_10 resolve 254 0.054 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off flat_10 render 1995 0.305 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off flat_100 resolve 1153 0.191 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off flat_100 render 18015 2.495 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off sparse_100 resolve 1171 0.194 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off sparse_100 render 16635 2.084 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off include_1 resolve 3743 0.781 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off include_1 render 26967 4.172 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off include_2 resolve 4258 1.005 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off include_2 render 36224 6.298 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off include_3 resolve 6961 1.612 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off include_3 render 48359 8.507 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off siblings_50 resolve 5752 18.262 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off siblings_50 render 37137 24.331 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off chain_and_sibling_50 resolve 6266 25.389 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off chain_and_sibling_50 render 46393 34.482 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off deep_and_sibling_50 resolve 8969 34.903 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off deep_and_sibling_50 render 58527 43.017 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off stats_100 resolve 1172 0.193 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 off stats_100 render 18038 2.446 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on flat_10 resolve 254 0.054 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on flat_10 render 1995 0.304 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on flat_100 resolve 1153 0.188 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on flat_100 render 18015 2.466 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on sparse_100 resolve 1171 0.193 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on sparse_100 render 16635 2.098 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on include_1 resolve 3786 10.563 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on include_1 render 27010 14.463 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on include_2 resolve 4344 12.838 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on include_2 render 36310 16.677 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on include_3 resolve 7090 13.631 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on include_3 render 48488 28.65 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on siblings_50 resolve 5838 16.305 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on siblings_50 render 37223 23.687 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on chain_and_sibling_50 resolve 6395 27.755 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on chain_and_sibling_50 render 46522 38.619 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on deep_and_sibling_50 resolve 9141 42.06 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on deep_and_sibling_50 render 58699 51.036 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on stats_100 resolve 1172 0.194 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.3.9 4.0 on stats_100 render 18038 2.441 Apple M1 Max/10 b14fc35c6bed 2b9685be19cf +v1.4.0 4.0 off flat_10 resolve 254 0.054 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off flat_10 render 1995 0.306 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off flat_100 resolve 1153 0.194 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off flat_100 render 18015 2.646 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off sparse_100 resolve 1171 0.202 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off sparse_100 render 16635 2.143 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off include_1 resolve 3747 0.791 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off include_1 render 26971 4.227 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off include_2 resolve 4272 1.014 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off include_2 render 36238 5.986 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off include_3 resolve 6993 1.542 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off include_3 render 48391 7.696 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off siblings_50 resolve 5758 18.635 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off siblings_50 render 37143 25.764 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off chain_and_sibling_50 resolve 6282 27.466 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off chain_and_sibling_50 render 46409 34.898 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off deep_and_sibling_50 resolve 9003 32.147 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off deep_and_sibling_50 render 58561 43.082 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off stats_100 resolve 1172 0.191 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 off stats_100 render 18038 2.481 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on flat_10 resolve 254 0.055 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on flat_10 render 1995 0.325 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on flat_100 resolve 1153 0.2 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on flat_100 render 18015 2.536 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on sparse_100 resolve 1171 0.196 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on sparse_100 render 16635 2.094 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on include_1 resolve 3790 10.427 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on include_1 render 27014 14.138 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on include_2 resolve 4358 13.736 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on include_2 render 36324 19.386 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on include_3 resolve 7122 26.181 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on include_3 render 48520 32.423 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on siblings_50 resolve 5844 17.027 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on siblings_50 render 37229 23.153 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on chain_and_sibling_50 resolve 6411 29.043 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on chain_and_sibling_50 render 46538 38.308 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on deep_and_sibling_50 resolve 9175 41.543 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on deep_and_sibling_50 render 58733 51.142 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on stats_100 resolve 1172 0.198 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.4.0 4.0 on stats_100 render 18038 2.638 Apple M1 Max/10 908628da3095 2b9685be19cf +v1.5.0 4.0 off flat_10 resolve 254 0.055 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off flat_10 render 1995 0.308 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off flat_100 resolve 1153 0.19 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off flat_100 render 18015 2.524 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off sparse_100 resolve 1171 0.193 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off sparse_100 render 16635 2.082 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off include_1 resolve 3747 0.766 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off include_1 render 26971 4.19 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off include_2 resolve 4272 1.011 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off include_2 render 36238 6.033 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off include_3 resolve 6993 1.539 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off include_3 render 48391 7.922 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off siblings_50 resolve 5758 19.038 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off siblings_50 render 37143 25.344 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off chain_and_sibling_50 resolve 6282 25.103 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off chain_and_sibling_50 render 46409 34.016 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off deep_and_sibling_50 resolve 9003 31.867 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off deep_and_sibling_50 render 58561 42.009 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off stats_100 resolve 1172 0.196 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 off stats_100 render 18038 2.468 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on flat_10 resolve 254 0.054 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on flat_10 render 1995 0.306 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on flat_100 resolve 1153 0.187 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on flat_100 render 18015 2.492 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on sparse_100 resolve 1171 0.19 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on sparse_100 render 16635 2.098 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on include_1 resolve 3790 10.396 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on include_1 render 27014 14.739 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on include_2 resolve 4358 13.181 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on include_2 render 36324 18.856 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on include_3 resolve 7122 22.205 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on include_3 render 48520 30.732 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on siblings_50 resolve 5844 16.087 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on siblings_50 render 37229 23.368 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on chain_and_sibling_50 resolve 6411 27.352 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on chain_and_sibling_50 render 46538 35.837 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on deep_and_sibling_50 resolve 9175 40.756 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on deep_and_sibling_50 render 58733 48.831 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on stats_100 resolve 1172 0.195 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.0 4.0 on stats_100 render 18038 2.522 Apple M1 Max/10 4c85651bc70a 2b9685be19cf +v1.5.1 4.0 off flat_10 resolve 254 0.054 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off flat_10 render 1995 0.309 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off flat_100 resolve 1153 0.188 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off flat_100 render 18015 2.514 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off sparse_100 resolve 1171 0.196 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off sparse_100 render 16635 2.117 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off include_1 resolve 3747 0.778 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off include_1 render 26971 4.294 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off include_2 resolve 4272 1.009 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off include_2 render 36238 6.557 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off include_3 resolve 6993 1.63 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off include_3 render 48391 8.429 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off siblings_50 resolve 5758 18.588 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off siblings_50 render 37143 23.857 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off chain_and_sibling_50 resolve 6282 25.18 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off chain_and_sibling_50 render 46409 32.299 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off deep_and_sibling_50 resolve 9003 31.496 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off deep_and_sibling_50 render 58561 43.021 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off stats_100 resolve 1172 0.194 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 off stats_100 render 18038 2.493 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on flat_10 resolve 254 0.054 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on flat_10 render 1995 0.308 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on flat_100 resolve 1153 0.19 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on flat_100 render 18015 2.509 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on sparse_100 resolve 1171 0.193 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on sparse_100 render 16635 2.101 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on include_1 resolve 3790 10.492 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on include_1 render 27014 14.487 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on include_2 resolve 4358 12.447 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on include_2 render 36324 17.69 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on include_3 resolve 7122 21.626 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on include_3 render 48520 30.734 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on siblings_50 resolve 5844 16.469 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on siblings_50 render 37229 23.136 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on chain_and_sibling_50 resolve 6411 28.957 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on chain_and_sibling_50 render 46538 37.297 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on deep_and_sibling_50 resolve 9175 42.119 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on deep_and_sibling_50 render 58733 51.18 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on stats_100 resolve 1172 0.194 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.1 4.0 on stats_100 render 18038 2.479 Apple M1 Max/10 9bfdf370c2ba 2b9685be19cf +v1.5.2 4.0 off flat_10 resolve 254 0.054 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off flat_10 render 1995 0.305 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off flat_100 resolve 1153 0.188 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off flat_100 render 18015 2.475 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off sparse_100 resolve 1171 0.198 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off sparse_100 render 16635 2.327 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off include_1 resolve 3747 0.812 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off include_1 render 26971 4.18 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off include_2 resolve 4272 1.011 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off include_2 render 36238 5.975 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off include_3 resolve 6993 1.548 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off include_3 render 48391 7.703 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off siblings_50 resolve 5758 18.206 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off siblings_50 render 37143 25.007 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off chain_and_sibling_50 resolve 6282 27.454 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off chain_and_sibling_50 render 46409 33.773 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off deep_and_sibling_50 resolve 9003 34.074 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off deep_and_sibling_50 render 58561 41.706 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off stats_100 resolve 1172 0.192 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 off stats_100 render 18038 2.466 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on flat_10 resolve 254 0.054 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on flat_10 render 1995 0.309 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on flat_100 resolve 1153 0.2 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on flat_100 render 18015 2.689 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on sparse_100 resolve 1171 0.204 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on sparse_100 render 16635 2.176 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on include_1 resolve 3790 10.411 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on include_1 render 27014 15.355 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on include_2 resolve 4358 12.476 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on include_2 render 36324 19.592 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on include_3 resolve 7122 22.334 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on include_3 render 48520 32.82 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on siblings_50 resolve 5844 16.819 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on siblings_50 render 37229 22.349 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on chain_and_sibling_50 resolve 6411 28.509 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on chain_and_sibling_50 render 46538 37.315 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on deep_and_sibling_50 resolve 9175 41.908 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on deep_and_sibling_50 render 58733 55.712 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on stats_100 resolve 1172 0.193 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.2 4.0 on stats_100 render 18038 2.606 Apple M1 Max/10 0282b1b27230 2b9685be19cf +v1.5.3 4.0 off flat_10 resolve 254 0.054 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off flat_10 render 1995 0.31 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off flat_100 resolve 1153 0.193 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off flat_100 render 18015 2.519 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off sparse_100 resolve 1171 0.197 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off sparse_100 render 16635 2.102 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off include_1 resolve 3747 0.78 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off include_1 render 26971 4.186 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off include_2 resolve 4272 1.008 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off include_2 render 36238 5.873 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off include_3 resolve 6993 1.535 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off include_3 render 48391 7.962 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off siblings_50 resolve 5758 19.548 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off siblings_50 render 37143 24.034 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off chain_and_sibling_50 resolve 6282 26.367 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off chain_and_sibling_50 render 46409 34.735 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off deep_and_sibling_50 resolve 9003 31.396 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off deep_and_sibling_50 render 58561 42.163 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off stats_100 resolve 1172 0.197 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 off stats_100 render 18038 2.483 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on flat_10 resolve 254 0.054 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on flat_10 render 1995 0.307 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on flat_100 resolve 1153 0.191 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on flat_100 render 18015 2.499 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on sparse_100 resolve 1171 0.194 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on sparse_100 render 16635 2.075 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on include_1 resolve 3790 10.469 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on include_1 render 27014 14.307 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on include_2 resolve 4358 13.363 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on include_2 render 36324 19.404 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on include_3 resolve 7122 22.956 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on include_3 render 48520 29.784 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on siblings_50 resolve 5844 16.488 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on siblings_50 render 37229 22.616 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on chain_and_sibling_50 resolve 6411 27.76 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on chain_and_sibling_50 render 46538 37.506 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on deep_and_sibling_50 resolve 9175 40.742 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on deep_and_sibling_50 render 58733 48.139 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on stats_100 resolve 1172 0.197 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.5.3 4.0 on stats_100 render 18038 2.452 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.0 4.0 off flat_10 resolve 254 0.054 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off flat_10 render 1995 0.304 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off flat_100 resolve 1153 0.191 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off flat_100 render 18015 2.497 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off sparse_100 resolve 1171 0.195 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off sparse_100 render 16635 2.08 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off include_1 resolve 3747 0.782 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off include_1 render 26971 4.192 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off include_2 resolve 4272 1.01 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off include_2 render 36238 5.98 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off include_3 resolve 6993 1.612 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off include_3 render 48391 8.861 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off siblings_50 resolve 5758 18.183 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off siblings_50 render 37143 25.079 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off chain_and_sibling_50 resolve 6282 26.666 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off chain_and_sibling_50 render 46409 34.818 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off deep_and_sibling_50 resolve 9003 33.353 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off deep_and_sibling_50 render 58561 43.304 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off stats_100 resolve 1172 0.196 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 off stats_100 render 18038 2.472 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on flat_10 resolve 254 0.054 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on flat_10 render 1995 0.304 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on flat_100 resolve 1153 0.19 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on flat_100 render 18015 2.436 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on sparse_100 resolve 1171 0.194 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on sparse_100 render 16635 2.098 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on include_1 resolve 3790 10.772 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on include_1 render 27014 14.975 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on include_2 resolve 4358 12.242 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on include_2 render 36324 18.277 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on include_3 resolve 7122 15.593 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on include_3 render 48520 31.195 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on siblings_50 resolve 5844 15.751 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on siblings_50 render 37229 22.057 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on chain_and_sibling_50 resolve 6411 27.448 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on chain_and_sibling_50 render 46538 36.527 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on deep_and_sibling_50 resolve 9175 42.892 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on deep_and_sibling_50 render 58733 53.455 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on stats_100 resolve 1172 0.195 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.0 4.0 on stats_100 render 18038 2.433 Apple M1 Max/10 4fd494cecdbd 2b9685be19cf +v1.6.1 4.0 off flat_10 resolve 254 0.054 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off flat_10 render 1995 0.303 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off flat_100 resolve 1153 0.191 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off flat_100 render 18015 2.437 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off sparse_100 resolve 1171 0.194 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off sparse_100 render 16635 2.268 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off include_1 resolve 3747 0.821 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off include_1 render 26971 4.381 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off include_2 resolve 4272 1.012 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off include_2 render 36238 6.07 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off include_3 resolve 6993 1.535 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off include_3 render 48391 7.857 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off siblings_50 resolve 5758 19.348 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off siblings_50 render 37143 24.516 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off chain_and_sibling_50 resolve 6282 26.306 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off chain_and_sibling_50 render 46409 35.488 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off deep_and_sibling_50 resolve 9003 34.312 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off deep_and_sibling_50 render 58561 42.302 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off stats_100 resolve 1172 0.197 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 off stats_100 render 18038 2.491 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on flat_10 resolve 254 0.055 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on flat_10 render 1995 0.308 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on flat_100 resolve 1153 0.192 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on flat_100 render 18015 2.504 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on sparse_100 resolve 1171 0.198 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on sparse_100 render 16635 2.097 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on include_1 resolve 3790 10.477 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on include_1 render 27014 14.313 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on include_2 resolve 4358 12.408 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on include_2 render 36324 17.225 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on include_3 resolve 7122 15.457 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on include_3 render 48520 33.397 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on siblings_50 resolve 5844 16.23 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on siblings_50 render 37229 22.823 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on chain_and_sibling_50 resolve 6411 30.353 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on chain_and_sibling_50 render 46538 36.223 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on deep_and_sibling_50 resolve 9175 39.863 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on deep_and_sibling_50 render 58733 50.842 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on stats_100 resolve 1172 0.192 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.1 4.0 on stats_100 render 18038 2.46 Apple M1 Max/10 4a77e6f3e3ee 2b9685be19cf +v1.6.2 4.0 off flat_10 resolve 254 0.056 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off flat_10 render 1995 0.306 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off flat_100 resolve 1153 0.19 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off flat_100 render 18015 2.462 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off sparse_100 resolve 1171 0.196 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off sparse_100 render 16635 2.077 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off include_1 resolve 3747 0.784 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off include_1 render 26971 4.155 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off include_2 resolve 4272 1.018 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off include_2 render 36238 5.95 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off include_3 resolve 6993 1.518 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off include_3 render 48391 7.876 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off siblings_50 resolve 5758 18.744 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off siblings_50 render 37143 23.508 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off chain_and_sibling_50 resolve 6282 24.793 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off chain_and_sibling_50 render 46409 33.718 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off deep_and_sibling_50 resolve 9003 30.979 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off deep_and_sibling_50 render 58561 40.871 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off stats_100 resolve 1172 0.205 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 off stats_100 render 18038 2.503 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on flat_10 resolve 254 0.054 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on flat_10 render 1995 0.308 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on flat_100 resolve 1153 0.191 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on flat_100 render 18015 2.477 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on sparse_100 resolve 1171 0.196 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on sparse_100 render 16635 2.107 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on include_1 resolve 3790 10.276 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on include_1 render 27014 14.278 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on include_2 resolve 4358 15.647 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on include_2 render 36324 17.369 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on include_3 resolve 7122 24.074 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on include_3 render 48520 29.448 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on siblings_50 resolve 5844 16.415 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on siblings_50 render 37229 22.725 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on chain_and_sibling_50 resolve 6411 26.923 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on chain_and_sibling_50 render 46538 34.405 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on deep_and_sibling_50 resolve 9175 40.199 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on deep_and_sibling_50 render 58733 48.425 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on stats_100 resolve 1172 0.197 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.2 4.0 on stats_100 render 18038 2.485 Apple M1 Max/10 5ef4e55a95ab 2b9685be19cf +v1.6.3 4.0 off flat_10 resolve 254 0.054 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off flat_10 render 1995 0.302 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off flat_100 resolve 1153 0.188 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off flat_100 render 18015 2.485 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off sparse_100 resolve 1171 0.193 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off sparse_100 render 16635 2.105 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off include_1 resolve 3747 0.77 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off include_1 render 26971 4.18 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off include_2 resolve 4272 0.995 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off include_2 render 36238 5.808 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off include_3 resolve 6993 1.574 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off include_3 render 48391 8.757 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off siblings_50 resolve 5758 17.515 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off siblings_50 render 37143 24.403 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off chain_and_sibling_50 resolve 6282 25.011 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off chain_and_sibling_50 render 46409 33.16 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off deep_and_sibling_50 resolve 9003 32.185 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off deep_and_sibling_50 render 58561 44.082 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off stats_100 resolve 1172 0.194 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 off stats_100 render 18038 2.437 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on flat_10 resolve 254 0.054 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on flat_10 render 1995 0.306 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on flat_100 resolve 1153 0.188 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on flat_100 render 18015 2.457 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on sparse_100 resolve 1171 0.194 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on sparse_100 render 16635 2.124 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on include_1 resolve 3790 10.478 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on include_1 render 27014 14.574 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on include_2 resolve 4358 12.611 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on include_2 render 36324 18.245 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on include_3 resolve 7122 14.843 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on include_3 render 48520 21.27 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on siblings_50 resolve 5844 16.262 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on siblings_50 render 37229 21.3 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on chain_and_sibling_50 resolve 6411 28.551 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on chain_and_sibling_50 render 46538 35.971 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on deep_and_sibling_50 resolve 9175 41.517 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on deep_and_sibling_50 render 58733 51.15 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on stats_100 resolve 1172 0.193 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.3 4.0 on stats_100 render 18038 2.442 Apple M1 Max/10 bb2ff07ec5be 2b9685be19cf +v1.6.4 4.0 off flat_10 resolve 254 0.053 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off flat_10 render 1995 0.302 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off flat_100 resolve 1153 0.187 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off flat_100 render 18015 2.445 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off sparse_100 resolve 1171 0.193 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off sparse_100 render 16635 2.265 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off include_1 resolve 3747 0.824 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off include_1 render 26971 4.11 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off include_2 resolve 4272 1.01 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off include_2 render 36238 5.854 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off include_3 resolve 6993 1.538 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off include_3 render 48391 8.454 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off siblings_50 resolve 5758 18.313 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off siblings_50 render 37143 25.077 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off chain_and_sibling_50 resolve 6282 25.245 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off chain_and_sibling_50 render 46409 35.423 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off deep_and_sibling_50 resolve 9003 32.186 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off deep_and_sibling_50 render 58561 43.231 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off stats_100 resolve 1172 0.191 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 off stats_100 render 18038 2.428 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on flat_10 resolve 254 0.054 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on flat_10 render 1995 0.302 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on flat_100 resolve 1153 0.192 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on flat_100 render 18015 2.668 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on sparse_100 resolve 1171 0.202 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on sparse_100 render 16635 2.12 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on include_1 resolve 3790 10.445 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on include_1 render 27014 14.467 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on include_2 resolve 4358 12.181 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on include_2 render 36324 18.872 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on include_3 resolve 7122 21.738 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on include_3 render 48520 31.549 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on siblings_50 resolve 5844 16.166 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on siblings_50 render 37229 22.714 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on chain_and_sibling_50 resolve 6411 27.276 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on chain_and_sibling_50 render 46538 37.293 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on deep_and_sibling_50 resolve 9175 40.428 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on deep_and_sibling_50 render 58733 52.748 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on stats_100 resolve 1172 0.192 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.6.4 4.0 on stats_100 render 18038 2.461 Apple M1 Max/10 965fd5c08577 2b9685be19cf +v1.7.0 4.0 off flat_10 resolve 256 0.056 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off flat_10 render 1997 0.315 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off flat_100 resolve 1155 0.197 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off flat_100 render 18017 2.451 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off sparse_100 resolve 1173 0.195 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off sparse_100 render 16637 2.09 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off include_1 resolve 3751 0.822 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off include_1 render 26975 4.26 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off include_2 resolve 4278 1.019 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off include_2 render 36244 5.728 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off include_3 resolve 7001 1.541 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off include_3 render 48399 7.568 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off siblings_50 resolve 5764 19.084 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off siblings_50 render 37149 26.0 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off chain_and_sibling_50 resolve 6290 24.496 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off chain_and_sibling_50 render 46417 35.14 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off deep_and_sibling_50 resolve 9013 32.602 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off deep_and_sibling_50 render 58571 42.818 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off stats_100 resolve 1174 0.204 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 off stats_100 render 18040 2.443 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on flat_10 resolve 256 0.055 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on flat_10 render 1997 0.306 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on flat_100 resolve 1155 0.191 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on flat_100 render 18017 2.455 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on sparse_100 resolve 1173 0.195 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on sparse_100 render 16637 2.133 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on include_1 resolve 3794 10.433 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on include_1 render 27018 14.656 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on include_2 resolve 4364 15.678 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on include_2 render 36330 20.43 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on include_3 resolve 7130 24.594 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on include_3 render 48528 31.832 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on siblings_50 resolve 5850 16.234 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on siblings_50 render 37235 23.412 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on chain_and_sibling_50 resolve 6419 27.543 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on chain_and_sibling_50 render 46546 36.967 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on deep_and_sibling_50 resolve 9185 40.969 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on deep_and_sibling_50 render 58743 48.507 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on stats_100 resolve 1174 0.195 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.0 4.0 on stats_100 render 18040 2.46 Apple M1 Max/10 42087b183c33 2b9685be19cf +v1.7.1 4.0 off flat_10 resolve 256 0.055 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off flat_10 render 1997 0.309 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off flat_100 resolve 1155 0.187 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off flat_100 render 18017 2.457 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off sparse_100 resolve 1173 0.194 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off sparse_100 render 16637 2.084 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off include_1 resolve 3751 0.767 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off include_1 render 26975 4.088 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off include_2 resolve 4278 0.99 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off include_2 render 36244 5.847 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off include_3 resolve 7001 1.525 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off include_3 render 48399 7.766 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off siblings_50 resolve 5764 18.047 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off siblings_50 render 37149 23.854 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off chain_and_sibling_50 resolve 6290 23.997 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off chain_and_sibling_50 render 46417 32.742 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off deep_and_sibling_50 resolve 9013 31.202 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off deep_and_sibling_50 render 58571 41.653 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off stats_100 resolve 1174 0.195 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 off stats_100 render 18040 2.468 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on flat_10 resolve 256 0.055 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on flat_10 render 1997 0.306 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on flat_100 resolve 1155 0.189 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on flat_100 render 18017 2.462 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on sparse_100 resolve 1173 0.192 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on sparse_100 render 16637 2.052 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on include_1 resolve 3794 10.624 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on include_1 render 27018 14.94 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on include_2 resolve 4364 11.512 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on include_2 render 36330 18.163 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on include_3 resolve 7130 20.985 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on include_3 render 48528 23.197 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on siblings_50 resolve 5850 15.964 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on siblings_50 render 37235 22.457 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on chain_and_sibling_50 resolve 6419 26.739 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on chain_and_sibling_50 render 46546 36.206 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on deep_and_sibling_50 resolve 9185 39.245 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on deep_and_sibling_50 render 58743 49.506 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on stats_100 resolve 1174 0.194 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.1 4.0 on stats_100 render 18040 2.462 Apple M1 Max/10 a939a74e55e6 2b9685be19cf +v1.7.2 4.0 off flat_10 resolve 256 0.054 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off flat_10 render 1997 0.302 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off flat_100 resolve 1155 0.187 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off flat_100 render 18017 2.428 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off sparse_100 resolve 1173 0.191 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off sparse_100 render 16637 2.108 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off include_1 resolve 3751 0.789 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off include_1 render 26975 4.511 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off include_2 resolve 4278 1.067 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off include_2 render 36244 5.845 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off include_3 resolve 7001 1.542 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off include_3 render 48399 7.961 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off siblings_50 resolve 5764 18.854 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off siblings_50 render 37149 25.385 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off chain_and_sibling_50 resolve 6290 25.371 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off chain_and_sibling_50 render 46417 34.361 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off deep_and_sibling_50 resolve 9013 36.913 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off deep_and_sibling_50 render 58571 43.152 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off stats_100 resolve 1174 0.193 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 off stats_100 render 18040 2.452 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on flat_10 resolve 256 0.054 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on flat_10 render 1997 0.304 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on flat_100 resolve 1155 0.19 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on flat_100 render 18017 2.447 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on sparse_100 resolve 1173 0.197 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on sparse_100 render 16637 2.272 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on include_1 resolve 3794 10.461 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on include_1 render 27018 14.339 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on include_2 resolve 4364 11.555 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on include_2 render 36330 18.041 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on include_3 resolve 7130 12.204 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on include_3 render 48528 30.041 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on siblings_50 resolve 5850 15.94 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on siblings_50 render 37235 23.352 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on chain_and_sibling_50 resolve 6419 28.073 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on chain_and_sibling_50 render 46546 36.131 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on deep_and_sibling_50 resolve 9185 37.34 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on deep_and_sibling_50 render 58743 48.086 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on stats_100 resolve 1174 0.195 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.2 4.0 on stats_100 render 18040 2.432 Apple M1 Max/10 76d7eb44a584 2b9685be19cf +v1.7.3 4.0 off flat_10 resolve 256 0.055 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off flat_10 render 1997 0.312 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off flat_100 resolve 1155 0.196 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off flat_100 render 18017 2.467 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off sparse_100 resolve 1173 0.195 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off sparse_100 render 16637 2.08 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off include_1 resolve 3751 0.783 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off include_1 render 26975 4.138 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off include_2 resolve 4278 1.01 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off include_2 render 36244 5.882 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off include_3 resolve 7001 1.558 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off include_3 render 48399 7.63 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off siblings_50 resolve 5764 18.859 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off siblings_50 render 37149 26.483 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off chain_and_sibling_50 resolve 6290 25.176 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off chain_and_sibling_50 render 46417 32.799 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off deep_and_sibling_50 resolve 9013 32.303 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off deep_and_sibling_50 render 58571 44.864 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off stats_100 resolve 1174 0.194 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 off stats_100 render 18040 2.521 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on flat_10 resolve 256 0.058 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on flat_10 render 1997 0.309 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on flat_100 resolve 1155 0.193 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on flat_100 render 18017 2.474 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on sparse_100 resolve 1173 0.197 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on sparse_100 render 16637 2.08 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on include_1 resolve 3794 10.46 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on include_1 render 27018 14.295 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on include_2 resolve 4364 15.117 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on include_2 render 36330 21.715 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on include_3 resolve 7130 15.951 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on include_3 render 48528 33.539 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on siblings_50 resolve 5850 16.219 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on siblings_50 render 37235 23.497 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on chain_and_sibling_50 resolve 6419 26.715 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on chain_and_sibling_50 render 46546 35.398 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on deep_and_sibling_50 resolve 9185 39.314 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on deep_and_sibling_50 render 58743 49.303 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on stats_100 resolve 1174 0.199 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.3 4.0 on stats_100 render 18040 2.481 Apple M1 Max/10 35bee7c39d20 2b9685be19cf +v1.7.4 4.0 off flat_10 resolve 256 0.055 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off flat_10 render 1997 0.308 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off flat_100 resolve 1155 0.191 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off flat_100 render 18017 2.472 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off sparse_100 resolve 1173 0.194 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off sparse_100 render 16637 2.074 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off include_1 resolve 3751 0.78 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off include_1 render 26975 4.158 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off include_2 resolve 4278 0.997 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off include_2 render 36244 5.855 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off include_3 resolve 7001 1.509 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off include_3 render 48399 7.741 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off siblings_50 resolve 5764 18.769 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off siblings_50 render 37149 25.157 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off chain_and_sibling_50 resolve 6290 24.214 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off chain_and_sibling_50 render 46417 31.877 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off deep_and_sibling_50 resolve 9013 32.05 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off deep_and_sibling_50 render 58571 40.448 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off stats_100 resolve 1174 0.195 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 off stats_100 render 18040 2.459 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on flat_10 resolve 256 0.055 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on flat_10 render 1997 0.308 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on flat_100 resolve 1155 0.189 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on flat_100 render 18017 2.461 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on sparse_100 resolve 1173 0.193 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on sparse_100 render 16637 2.08 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on include_1 resolve 3794 10.422 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on include_1 render 27018 14.183 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on include_2 resolve 4364 13.875 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on include_2 render 36330 19.329 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on include_3 resolve 7130 16.19 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on include_3 render 48528 24.5 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on siblings_50 resolve 5850 15.759 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on siblings_50 render 37235 22.427 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on chain_and_sibling_50 resolve 6419 27.556 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on chain_and_sibling_50 render 46546 36.441 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on deep_and_sibling_50 resolve 9185 39.597 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on deep_and_sibling_50 render 58743 50.144 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on stats_100 resolve 1174 0.196 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.4 4.0 on stats_100 render 18040 2.441 Apple M1 Max/10 27780b2ada1d 2b9685be19cf +v1.7.5 4.0 off flat_10 resolve 256 0.054 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off flat_10 render 1997 0.302 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off flat_100 resolve 1155 0.189 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off flat_100 render 18017 2.438 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off sparse_100 resolve 1173 0.192 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off sparse_100 render 16637 2.096 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off include_1 resolve 3751 0.766 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off include_1 render 26975 4.089 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off include_2 resolve 4278 1.022 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off include_2 render 36244 6.344 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off include_3 resolve 7001 1.618 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off include_3 render 48399 7.808 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off siblings_50 resolve 5764 18.444 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off siblings_50 render 37149 23.96 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off chain_and_sibling_50 resolve 6290 26.135 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off chain_and_sibling_50 render 46417 32.524 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off deep_and_sibling_50 resolve 9013 33.442 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off deep_and_sibling_50 render 58571 42.768 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off stats_100 resolve 1174 0.194 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 off stats_100 render 18040 2.445 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on flat_10 resolve 256 0.054 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on flat_10 render 1997 0.304 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on flat_100 resolve 1155 0.187 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on flat_100 render 18017 2.446 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on sparse_100 resolve 1173 0.191 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on sparse_100 render 16637 2.105 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on include_1 resolve 3794 10.442 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on include_1 render 27018 15.074 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on include_2 resolve 4364 11.935 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on include_2 render 36330 17.741 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on include_3 resolve 7130 14.422 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on include_3 render 48528 30.653 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on siblings_50 resolve 5850 16.367 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on siblings_50 render 37235 21.75 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on chain_and_sibling_50 resolve 6419 27.894 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on chain_and_sibling_50 render 46546 36.749 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on deep_and_sibling_50 resolve 9185 41.279 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on deep_and_sibling_50 render 58743 51.754 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on stats_100 resolve 1174 0.192 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.5 4.0 on stats_100 render 18040 2.44 Apple M1 Max/10 eb98c84b76b1 2b9685be19cf +v1.7.6 4.0 off flat_10 resolve 256 0.054 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off flat_10 render 1997 0.301 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off flat_100 resolve 1155 0.191 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off flat_100 render 18017 2.588 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off sparse_100 resolve 1173 0.199 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off sparse_100 render 16637 2.124 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off include_1 resolve 3751 0.786 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off include_1 render 26975 4.121 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off include_2 resolve 4278 1.019 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off include_2 render 36244 5.876 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off include_3 resolve 7001 1.549 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off include_3 render 48399 7.774 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off siblings_50 resolve 5764 18.745 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off siblings_50 render 37149 26.04 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off chain_and_sibling_50 resolve 6290 24.823 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off chain_and_sibling_50 render 46417 35.504 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off deep_and_sibling_50 resolve 9013 35.901 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off deep_and_sibling_50 render 58571 45.03 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off stats_100 resolve 1174 0.193 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 off stats_100 render 18040 2.434 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on flat_10 resolve 256 0.056 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on flat_10 render 1997 0.32 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on flat_100 resolve 1155 0.2 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on flat_100 render 18017 2.494 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on sparse_100 resolve 1173 0.197 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on sparse_100 render 16637 2.076 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on include_1 resolve 3794 10.425 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on include_1 render 27018 14.387 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on include_2 resolve 4364 12.925 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on include_2 render 36330 18.735 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on include_3 resolve 7130 15.495 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on include_3 render 48528 37.091 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on siblings_50 resolve 5850 16.219 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on siblings_50 render 37235 23.107 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on chain_and_sibling_50 resolve 6419 29.76 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on chain_and_sibling_50 render 46546 36.54 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on deep_and_sibling_50 resolve 9185 43.064 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on deep_and_sibling_50 render 58743 48.279 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on stats_100 resolve 1174 0.195 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.6 4.0 on stats_100 render 18040 2.519 Apple M1 Max/10 3dfee6c12409 2b9685be19cf +v1.7.7 4.0 off flat_10 resolve 256 0.055 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off flat_10 render 1997 0.307 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off flat_100 resolve 1155 0.192 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off flat_100 render 18017 2.46 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off sparse_100 resolve 1173 0.196 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off sparse_100 render 16637 2.077 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off include_1 resolve 3751 0.776 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off include_1 render 26975 4.112 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off include_2 resolve 4278 0.993 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off include_2 render 36244 5.855 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off include_3 resolve 7001 1.534 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off include_3 render 48399 7.923 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off siblings_50 resolve 5764 18.797 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off siblings_50 render 37149 24.45 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off chain_and_sibling_50 resolve 6290 22.973 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off chain_and_sibling_50 render 46417 31.614 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off deep_and_sibling_50 resolve 9013 30.44 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off deep_and_sibling_50 render 58571 41.112 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off stats_100 resolve 1174 0.195 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 off stats_100 render 18040 2.455 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on flat_10 resolve 256 0.055 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on flat_10 render 1997 0.307 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on flat_100 resolve 1155 0.191 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on flat_100 render 18017 2.635 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on sparse_100 resolve 1173 0.196 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on sparse_100 render 16637 2.157 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on include_1 resolve 3794 10.285 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on include_1 render 27018 14.047 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on include_2 resolve 4364 12.896 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on include_2 render 36330 19.072 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on include_3 resolve 7130 13.197 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on include_3 render 48528 30.237 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on siblings_50 resolve 5850 15.927 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on siblings_50 render 37235 21.24 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on chain_and_sibling_50 resolve 6419 26.001 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on chain_and_sibling_50 render 46546 34.66 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on deep_and_sibling_50 resolve 9185 37.961 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on deep_and_sibling_50 render 58743 47.658 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on stats_100 resolve 1174 0.195 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.7 4.0 on stats_100 render 18040 2.46 Apple M1 Max/10 51728812e7ab 2b9685be19cf +v1.7.8 4.0 off flat_10 resolve 256 0.055 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off flat_10 render 1997 0.303 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off flat_100 resolve 1155 0.19 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off flat_100 render 18017 2.479 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off sparse_100 resolve 1173 0.193 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off sparse_100 render 16637 2.078 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off include_1 resolve 3751 0.777 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off include_1 render 26975 4.147 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off include_2 resolve 4278 0.99 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off include_2 render 36244 6.0 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off include_3 resolve 7001 1.541 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off include_3 render 48399 8.346 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off siblings_50 resolve 5764 17.533 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off siblings_50 render 37149 23.682 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off chain_and_sibling_50 resolve 6290 23.437 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off chain_and_sibling_50 render 46417 33.195 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off deep_and_sibling_50 resolve 9013 30.323 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off deep_and_sibling_50 render 58571 40.565 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off stats_100 resolve 1174 0.196 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 off stats_100 render 18040 2.484 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on flat_10 resolve 256 0.055 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on flat_10 render 1997 0.306 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on flat_100 resolve 1155 0.191 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on flat_100 render 18017 2.458 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on sparse_100 resolve 1173 0.192 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on sparse_100 render 16637 2.075 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on include_1 resolve 3794 10.36 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on include_1 render 27018 14.497 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on include_2 resolve 4364 11.691 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on include_2 render 36330 18.067 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on include_3 resolve 7130 21.142 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on include_3 render 48528 28.742 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on siblings_50 resolve 5850 15.806 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on siblings_50 render 37235 21.72 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on chain_and_sibling_50 resolve 6419 27.167 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on chain_and_sibling_50 render 46546 33.648 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on deep_and_sibling_50 resolve 9185 37.818 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on deep_and_sibling_50 render 58743 45.52 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on stats_100 resolve 1174 0.195 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.8 4.0 on stats_100 render 18040 2.483 Apple M1 Max/10 d943bda8142e 2b9685be19cf +v1.7.9 4.0 off flat_10 resolve 256 0.055 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off flat_10 render 1997 0.304 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off flat_100 resolve 1155 0.188 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off flat_100 render 18017 2.454 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off sparse_100 resolve 1173 0.196 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off sparse_100 render 16637 2.229 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off include_1 resolve 3751 0.819 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off include_1 render 26975 4.122 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off include_2 resolve 4278 1.009 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off include_2 render 36244 5.959 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off include_3 resolve 7001 1.541 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off include_3 render 48399 7.784 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off siblings_50 resolve 5764 18.558 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off siblings_50 render 37149 22.921 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off chain_and_sibling_50 resolve 6290 24.32 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off chain_and_sibling_50 render 46417 31.923 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off deep_and_sibling_50 resolve 9013 31.374 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off deep_and_sibling_50 render 58571 40.592 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off stats_100 resolve 1174 0.194 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 off stats_100 render 18040 2.45 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on flat_10 resolve 256 0.055 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on flat_10 render 1997 0.304 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on flat_100 resolve 1155 0.192 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on flat_100 render 18017 2.598 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on sparse_100 resolve 1173 0.199 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on sparse_100 render 16637 2.081 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on include_1 resolve 3794 10.425 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on include_1 render 27018 14.21 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on include_2 resolve 4364 11.41 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on include_2 render 36330 17.621 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on include_3 resolve 7130 21.48 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on include_3 render 48528 28.373 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on siblings_50 resolve 5850 16.889 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on siblings_50 render 37235 20.627 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on chain_and_sibling_50 resolve 6419 26.146 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on chain_and_sibling_50 render 46546 36.149 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on deep_and_sibling_50 resolve 9185 40.595 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on deep_and_sibling_50 render 58743 50.148 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on stats_100 resolve 1174 0.193 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.7.9 4.0 on stats_100 render 18040 2.463 Apple M1 Max/10 bacec08ecae0 2b9685be19cf +v1.8.0 4.0 off flat_10 resolve 353 0.089 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off flat_10 render 2094 0.343 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off flat_100 resolve 1252 0.22 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off flat_100 render 18114 2.479 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off sparse_100 resolve 1270 0.226 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off sparse_100 render 16734 2.123 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off include_1 resolve 4091 0.891 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off include_1 render 27315 4.287 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off include_2 resolve 4862 1.196 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off include_2 render 36828 6.087 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off include_3 resolve 7829 1.795 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off include_3 render 49227 7.898 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off siblings_50 resolve 6351 18.992 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off siblings_50 render 37736 24.035 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off chain_and_sibling_50 resolve 7121 22.94 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off chain_and_sibling_50 render 47248 33.144 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off deep_and_sibling_50 resolve 10088 29.529 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off deep_and_sibling_50 render 59646 40.393 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off stats_100 resolve 1271 0.234 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 off stats_100 render 18137 2.695 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on flat_10 resolve 351 0.098 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on flat_10 render 2092 0.359 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on flat_100 resolve 1250 0.234 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on flat_100 render 18112 2.511 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on sparse_100 resolve 1268 0.239 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on sparse_100 render 16732 2.154 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on include_1 resolve 4101 0.943 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on include_1 render 27325 4.371 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on include_2 resolve 4883 1.28 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on include_2 render 36849 6.228 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on include_3 resolve 7861 1.906 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on include_3 render 49259 8.179 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on siblings_50 resolve 6369 12.148 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on siblings_50 render 37754 17.613 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on chain_and_sibling_50 resolve 7150 18.105 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on chain_and_sibling_50 render 47277 25.824 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on deep_and_sibling_50 resolve 10128 24.694 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on deep_and_sibling_50 render 59686 34.925 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on stats_100 resolve 1269 0.241 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.0 4.0 on stats_100 render 18135 2.626 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off flat_10 resolve 353 0.085 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off flat_10 render 2094 0.341 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off flat_100 resolve 1252 0.22 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off flat_100 render 18114 2.465 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off sparse_100 resolve 1270 0.224 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off sparse_100 render 16734 2.109 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off include_1 resolve 4091 0.888 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off include_1 render 27315 4.238 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off include_2 resolve 4862 1.188 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off include_2 render 36828 6.057 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off include_3 resolve 7829 1.78 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off include_3 render 49227 7.874 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off siblings_50 resolve 6351 18.348 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off siblings_50 render 37736 24.076 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off chain_and_sibling_50 resolve 7121 25.004 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off chain_and_sibling_50 render 47248 32.306 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off deep_and_sibling_50 resolve 10088 31.345 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off deep_and_sibling_50 render 59646 40.166 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off stats_100 resolve 1271 0.225 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 off stats_100 render 18137 2.527 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on flat_10 resolve 351 0.098 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on flat_10 render 2092 0.357 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on flat_100 resolve 1250 0.236 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on flat_100 render 18112 2.478 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on sparse_100 resolve 1268 0.239 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on sparse_100 render 16732 2.129 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on include_1 resolve 4101 0.942 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on include_1 render 27325 4.341 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on include_2 resolve 4883 1.27 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on include_2 render 36849 6.099 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on include_3 resolve 7861 1.93 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on include_3 render 49259 9.121 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on siblings_50 resolve 6369 12.453 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on siblings_50 render 37754 18.32 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on chain_and_sibling_50 resolve 7150 18.047 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on chain_and_sibling_50 render 47277 25.949 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on deep_and_sibling_50 resolve 10128 24.021 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on deep_and_sibling_50 render 59686 34.933 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on stats_100 resolve 1269 0.241 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.1 4.0 on stats_100 render 18135 2.574 Apple M1 Max/10 a567f5d72bde 2b9685be19cf +v1.8.2 4.0 off flat_10 resolve 353 0.084 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off flat_10 render 2094 0.335 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off flat_100 resolve 1252 0.22 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off flat_100 render 18114 2.435 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off sparse_100 resolve 1270 0.223 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off sparse_100 render 16734 2.102 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off include_1 resolve 4093 0.883 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off include_1 render 27317 4.32 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off include_2 resolve 4866 1.267 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off include_2 render 36832 6.641 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off include_3 resolve 7835 1.813 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off include_3 render 49233 8.058 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off siblings_50 resolve 6355 18.376 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off siblings_50 render 37740 23.391 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off chain_and_sibling_50 resolve 7127 23.701 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off chain_and_sibling_50 render 47254 32.776 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off deep_and_sibling_50 resolve 10096 30.997 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off deep_and_sibling_50 render 59654 41.757 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off stats_100 resolve 1271 0.224 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 off stats_100 render 18137 2.511 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on flat_10 resolve 351 0.098 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on flat_10 render 2092 0.355 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on flat_100 resolve 1250 0.233 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on flat_100 render 18112 2.486 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on sparse_100 resolve 1268 0.238 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on sparse_100 render 16732 2.144 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on include_1 resolve 4097 1.015 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on include_1 render 27321 4.839 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on include_2 resolve 4875 1.338 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on include_2 render 36841 6.143 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on include_3 resolve 7849 1.924 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on include_3 render 49247 8.314 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on siblings_50 resolve 6361 12.355 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on siblings_50 render 37746 18.164 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on chain_and_sibling_50 resolve 7138 17.992 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on chain_and_sibling_50 render 47265 25.836 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on deep_and_sibling_50 resolve 10112 25.282 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on deep_and_sibling_50 render 59670 35.487 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on stats_100 resolve 1269 0.239 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.8.2 4.0 on stats_100 render 18135 2.501 Apple M1 Max/10 ad8c63bdfd85 2b9685be19cf +v1.9.0 4.0 off flat_10 resolve 353 0.085 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off flat_10 render 2094 0.342 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off flat_100 resolve 1252 0.222 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off flat_100 render 18114 2.588 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off sparse_100 resolve 1270 0.233 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off sparse_100 render 16734 2.265 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off include_1 resolve 4093 0.906 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off include_1 render 27317 4.303 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off include_2 resolve 4866 1.202 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off include_2 render 36832 6.155 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off include_3 resolve 7835 1.814 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off include_3 render 49233 8.099 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off siblings_50 resolve 6355 18.084 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off siblings_50 render 37740 24.542 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off chain_and_sibling_50 resolve 7127 26.119 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off chain_and_sibling_50 render 47254 33.708 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off deep_and_sibling_50 resolve 10096 33.771 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off deep_and_sibling_50 render 59654 41.845 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off stats_100 resolve 1271 0.225 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 off stats_100 render 18137 2.547 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on flat_10 resolve 351 0.097 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on flat_10 render 2092 0.385 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on flat_100 resolve 1250 0.247 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on flat_100 render 18112 2.689 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on sparse_100 resolve 1268 0.238 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on sparse_100 render 16732 2.153 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on include_1 resolve 4097 0.943 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on include_1 render 27321 4.357 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on include_2 resolve 4875 1.278 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on include_2 render 36841 6.332 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on include_3 resolve 7849 1.921 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on include_3 render 49247 9.118 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on siblings_50 resolve 6361 12.436 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on siblings_50 render 37746 17.403 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on chain_and_sibling_50 resolve 7138 18.952 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on chain_and_sibling_50 render 47265 26.252 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on deep_and_sibling_50 resolve 10112 27.055 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on deep_and_sibling_50 render 59670 34.394 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on stats_100 resolve 1269 0.236 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.9.0 4.0 on stats_100 render 18135 2.57 Apple M1 Max/10 856b568cefbd 2b9685be19cf +v1.10.0 4.0 off flat_10 resolve 353 0.09 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off flat_10 render 2094 0.341 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off flat_100 resolve 1252 0.221 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off flat_100 render 18114 2.466 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off sparse_100 resolve 1270 0.226 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off sparse_100 render 16734 2.136 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off include_1 resolve 4093 0.901 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off include_1 render 27317 4.255 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off include_2 resolve 4866 1.2 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off include_2 render 36832 6.125 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off include_3 resolve 7835 1.796 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off include_3 render 49233 7.866 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off siblings_50 resolve 6355 18.655 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off siblings_50 render 37740 25.973 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off chain_and_sibling_50 resolve 7127 26.628 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off chain_and_sibling_50 render 47254 33.672 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off deep_and_sibling_50 resolve 10096 30.978 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off deep_and_sibling_50 render 59654 42.676 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off stats_100 resolve 1271 0.23 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 off stats_100 render 18137 2.542 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on flat_10 resolve 351 0.098 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on flat_10 render 2092 0.355 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on flat_100 resolve 1250 0.234 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on flat_100 render 18112 2.499 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on sparse_100 resolve 1268 0.239 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on sparse_100 render 16732 2.142 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on include_1 resolve 4097 0.95 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on include_1 render 27321 4.344 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on include_2 resolve 4875 1.274 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on include_2 render 36841 6.283 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on include_3 resolve 7849 1.919 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on include_3 render 49247 8.382 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on siblings_50 resolve 6361 12.621 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on siblings_50 render 37746 18.731 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on chain_and_sibling_50 resolve 7138 19.021 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on chain_and_sibling_50 render 47265 26.635 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on deep_and_sibling_50 resolve 10112 25.787 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on deep_and_sibling_50 render 59670 35.475 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on stats_100 resolve 1269 0.24 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.0 4.0 on stats_100 render 18135 2.552 Apple M1 Max/10 1b258c5deb13 2b9685be19cf +v1.10.1 4.0 off flat_10 resolve 353 0.086 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off flat_10 render 2094 0.344 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off flat_100 resolve 1252 0.223 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off flat_100 render 18114 2.481 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off sparse_100 resolve 1270 0.227 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off sparse_100 render 16734 2.124 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off include_1 resolve 4093 0.898 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off include_1 render 27317 4.248 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off include_2 resolve 4866 1.2 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off include_2 render 36832 6.031 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off include_3 resolve 7835 1.814 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off include_3 render 49233 7.913 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off siblings_50 resolve 6355 18.27 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off siblings_50 render 37740 23.604 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off chain_and_sibling_50 resolve 7127 24.571 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off chain_and_sibling_50 render 47254 33.552 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off deep_and_sibling_50 resolve 10096 32.687 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off deep_and_sibling_50 render 59654 43.133 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off stats_100 resolve 1271 0.228 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 off stats_100 render 18137 2.531 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on flat_10 resolve 351 0.096 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on flat_10 render 2092 0.356 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on flat_100 resolve 1250 0.234 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on flat_100 render 18112 2.487 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on sparse_100 resolve 1268 0.239 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on sparse_100 render 16732 2.13 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on include_1 resolve 4097 0.945 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on include_1 render 27321 4.331 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on include_2 resolve 4875 1.275 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on include_2 render 36841 6.128 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on include_3 resolve 7849 2.115 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on include_3 render 49247 9.135 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on siblings_50 resolve 6370 12.927 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on siblings_50 render 37746 18.785 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on chain_and_sibling_50 resolve 7138 19.795 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on chain_and_sibling_50 render 47265 27.101 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on deep_and_sibling_50 resolve 10112 26.526 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on deep_and_sibling_50 render 59670 35.152 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on stats_100 resolve 1269 0.241 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.1 4.0 on stats_100 render 18135 2.558 Apple M1 Max/10 92f442776492 2b9685be19cf +v1.10.2 4.0 off flat_10 resolve 353 0.085 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off flat_10 render 2094 0.343 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off flat_100 resolve 1252 0.222 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off flat_100 render 18114 2.471 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off sparse_100 resolve 1270 0.227 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off sparse_100 render 16734 2.124 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off include_1 resolve 4093 0.894 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off include_1 render 27317 4.607 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off include_2 resolve 4866 1.234 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off include_2 render 36832 6.589 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off include_3 resolve 7835 1.809 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off include_3 render 49233 8.055 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off siblings_50 resolve 6355 18.883 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off siblings_50 render 37740 24.957 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off chain_and_sibling_50 resolve 7127 25.001 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off chain_and_sibling_50 render 47254 33.194 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off deep_and_sibling_50 resolve 10096 33.041 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off deep_and_sibling_50 render 59654 42.462 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off stats_100 resolve 1271 0.227 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 off stats_100 render 18137 2.52 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on flat_10 resolve 351 0.097 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on flat_10 render 2092 0.355 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on flat_100 resolve 1250 0.233 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on flat_100 render 18112 2.479 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on sparse_100 resolve 1268 0.238 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on sparse_100 render 16732 2.231 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on include_1 resolve 4097 1.002 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on include_1 render 27321 4.751 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on include_2 resolve 4875 1.279 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on include_2 render 36841 6.168 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on include_3 resolve 7849 1.919 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on include_3 render 49247 8.86 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on siblings_50 resolve 6361 12.302 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on siblings_50 render 37746 18.172 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on chain_and_sibling_50 resolve 7138 18.816 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on chain_and_sibling_50 render 47265 27.573 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on deep_and_sibling_50 resolve 10112 25.222 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on deep_and_sibling_50 render 59670 36.751 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on stats_100 resolve 1269 0.239 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.2 4.0 on stats_100 render 18135 2.502 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off flat_10 resolve 353 0.085 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off flat_10 render 2094 0.342 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off flat_100 resolve 1252 0.225 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off flat_100 render 18114 2.487 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off sparse_100 resolve 1270 0.228 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off sparse_100 render 16734 2.124 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off include_1 resolve 4093 0.898 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off include_1 render 27317 4.265 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off include_2 resolve 4866 1.199 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off include_2 render 36832 6.044 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off include_3 resolve 7835 1.805 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off include_3 render 49233 7.908 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off siblings_50 resolve 6355 18.914 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off siblings_50 render 37740 23.902 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off chain_and_sibling_50 resolve 7127 26.275 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off chain_and_sibling_50 render 47254 33.417 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off deep_and_sibling_50 resolve 10096 36.081 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off deep_and_sibling_50 render 59654 42.965 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off stats_100 resolve 1271 0.225 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 off stats_100 render 18137 2.515 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on flat_10 resolve 351 0.098 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on flat_10 render 2092 0.353 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on flat_100 resolve 1250 0.233 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on flat_100 render 18112 2.492 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on sparse_100 resolve 1268 0.237 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on sparse_100 render 16732 2.148 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on include_1 resolve 4097 0.944 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on include_1 render 27321 4.304 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on include_2 resolve 4875 1.267 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on include_2 render 36841 6.194 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on include_3 resolve 7849 1.892 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on include_3 render 49247 8.053 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on siblings_50 resolve 6361 12.943 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on siblings_50 render 37746 19.239 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on chain_and_sibling_50 resolve 7138 21.508 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on chain_and_sibling_50 render 47265 26.393 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on deep_and_sibling_50 resolve 10112 28.022 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on deep_and_sibling_50 render 59670 35.913 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on stats_100 resolve 1269 0.237 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.10.3 4.0 on stats_100 render 18135 2.546 Apple M1 Max/10 bcee4247171f 2b9685be19cf +v1.11.0 4.0 off flat_10 resolve 353 0.084 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off flat_10 render 2094 0.335 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off flat_100 resolve 1252 0.217 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off flat_100 render 18114 2.436 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off sparse_100 resolve 1270 0.221 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off sparse_100 render 16734 2.079 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off include_1 resolve 4093 0.877 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off include_1 render 27317 4.243 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off include_2 resolve 4866 1.179 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off include_2 render 36832 6.074 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off include_3 resolve 7835 1.776 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off include_3 render 49233 7.83 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off siblings_50 resolve 6355 19.017 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off siblings_50 render 37740 24.929 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off chain_and_sibling_50 resolve 7127 25.78 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off chain_and_sibling_50 render 47254 32.612 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off deep_and_sibling_50 resolve 10096 32.574 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off deep_and_sibling_50 render 59654 40.173 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off stats_100 resolve 1271 0.222 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 off stats_100 render 18137 2.458 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on flat_10 resolve 351 0.097 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on flat_10 render 2092 0.35 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on flat_100 resolve 1250 0.231 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on flat_100 render 18112 2.482 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on sparse_100 resolve 1268 0.234 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on sparse_100 render 16732 2.128 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on include_1 resolve 4097 0.929 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on include_1 render 27321 4.311 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on include_2 resolve 4875 1.259 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on include_2 render 36841 6.117 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on include_3 resolve 7849 1.895 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on include_3 render 49247 8.089 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on siblings_50 resolve 6361 12.628 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on siblings_50 render 37746 18.819 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on chain_and_sibling_50 resolve 7138 18.499 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on chain_and_sibling_50 render 47265 26.35 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on deep_and_sibling_50 resolve 10112 26.469 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on deep_and_sibling_50 render 59670 34.507 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on stats_100 resolve 1269 0.233 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.0 4.0 on stats_100 render 18135 2.498 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off flat_10 resolve 353 0.083 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off flat_10 render 2094 0.335 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off flat_100 resolve 1252 0.217 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off flat_100 render 18114 2.428 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off sparse_100 resolve 1270 0.222 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off sparse_100 render 16734 2.071 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off include_1 resolve 4093 0.875 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off include_1 render 27317 4.181 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off include_2 resolve 4866 1.172 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off include_2 render 36832 5.988 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off include_3 resolve 7835 1.764 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off include_3 render 49233 7.796 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off siblings_50 resolve 6355 18.406 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off siblings_50 render 37740 23.447 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off chain_and_sibling_50 resolve 7127 28.06 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off chain_and_sibling_50 render 47254 31.955 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off deep_and_sibling_50 resolve 10096 32.692 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off deep_and_sibling_50 render 59654 41.687 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off stats_100 resolve 1271 0.222 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 off stats_100 render 18137 2.457 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on flat_10 resolve 351 0.097 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on flat_10 render 2092 0.349 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on flat_100 resolve 1250 0.23 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on flat_100 render 18112 2.441 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on sparse_100 resolve 1268 0.234 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on sparse_100 render 16732 2.095 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on include_1 resolve 4097 0.929 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on include_1 render 27321 4.243 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on include_2 resolve 4875 1.257 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on include_2 render 36841 5.993 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on include_3 resolve 7849 1.875 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on include_3 render 49247 7.944 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on siblings_50 resolve 6361 13.207 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on siblings_50 render 37746 17.847 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on chain_and_sibling_50 resolve 7138 20.111 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on chain_and_sibling_50 render 47265 26.462 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on deep_and_sibling_50 resolve 10112 26.323 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on deep_and_sibling_50 render 59670 35.179 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on stats_100 resolve 1269 0.236 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.11.1 4.0 on stats_100 render 18135 2.491 Apple M1 Max/10 685aeac6b896 2b9685be19cf +v1.12.0 4.0 off flat_10 resolve 354 0.083 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off flat_10 render 2145 0.349 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off flat_100 resolve 1253 0.216 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off flat_100 render 18615 2.604 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off sparse_100 resolve 1271 0.221 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off sparse_100 render 17235 2.195 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off include_1 resolve 4095 0.893 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off include_1 render 27769 4.328 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off include_2 resolve 4869 1.195 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off include_2 render 37289 6.029 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off include_3 resolve 7839 1.785 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off include_3 render 49691 8.025 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off siblings_50 resolve 6358 21.59 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off siblings_50 render 38293 26.256 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off chain_and_sibling_50 resolve 7131 30.666 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off chain_and_sibling_50 render 47812 34.391 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off deep_and_sibling_50 resolve 10101 33.51 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off deep_and_sibling_50 render 60213 40.354 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off stats_100 resolve 1272 0.221 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 off stats_100 render 18638 2.586 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on flat_10 resolve 352 0.098 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on flat_10 render 2143 0.364 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on flat_100 resolve 1251 0.23 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on flat_100 render 18613 2.579 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on sparse_100 resolve 1269 0.236 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on sparse_100 render 17233 2.22 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on include_1 resolve 4099 0.945 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on include_1 render 27773 4.402 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on include_2 resolve 4878 1.286 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on include_2 render 37298 6.258 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on include_3 resolve 7853 1.923 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on include_3 render 49705 8.098 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on siblings_50 resolve 6364 12.589 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on siblings_50 render 38299 18.411 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on chain_and_sibling_50 resolve 7142 19.692 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on chain_and_sibling_50 render 47823 25.98 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on deep_and_sibling_50 resolve 10117 27.293 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on deep_and_sibling_50 render 60229 34.095 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on stats_100 resolve 1270 0.236 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.0 4.0 on stats_100 render 18636 2.631 Apple M1 Max/10 2f1b163135a1 2b9685be19cf +v1.12.1 4.0 off flat_10 resolve 257 0.054 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off flat_10 render 2048 0.317 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off flat_100 resolve 1156 0.188 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off flat_100 render 18518 2.567 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off sparse_100 resolve 1174 0.193 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off sparse_100 render 17138 2.214 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off include_1 resolve 3749 0.793 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off include_1 render 27423 4.19 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off include_2 resolve 4273 1.013 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off include_2 render 36693 5.883 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off include_3 resolve 6993 1.536 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off include_3 render 48845 7.811 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off siblings_50 resolve 5761 19.364 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off siblings_50 render 37696 25.057 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off chain_and_sibling_50 resolve 6284 26.571 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off chain_and_sibling_50 render 46965 31.376 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off deep_and_sibling_50 resolve 9004 33.391 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off deep_and_sibling_50 render 59116 43.602 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off stats_100 resolve 1175 0.193 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 off stats_100 render 18541 2.564 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on flat_10 resolve 352 0.098 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on flat_10 render 2143 0.365 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on flat_100 resolve 1251 0.231 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on flat_100 render 18613 2.594 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on sparse_100 resolve 1269 0.235 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on sparse_100 render 17233 2.229 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on include_1 resolve 4099 0.944 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on include_1 render 27773 4.383 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on include_2 resolve 4878 1.281 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on include_2 render 37298 6.305 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on include_3 resolve 7853 1.916 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on include_3 render 49705 8.058 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on siblings_50 resolve 6373 12.839 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on siblings_50 render 38299 19.248 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on chain_and_sibling_50 resolve 7142 19.004 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on chain_and_sibling_50 render 47823 27.213 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on deep_and_sibling_50 resolve 10117 27.184 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on deep_and_sibling_50 render 60229 35.845 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on stats_100 resolve 1270 0.237 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.1 4.0 on stats_100 render 18636 2.65 Apple M1 Max/10 58ae88ac865b 2b9685be19cf +v1.12.2 4.0 off flat_10 resolve 257 0.054 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off flat_10 render 2048 0.316 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off flat_100 resolve 1156 0.187 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off flat_100 render 18518 2.564 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off sparse_100 resolve 1174 0.192 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off sparse_100 render 17138 2.192 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off include_1 resolve 3749 0.784 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off include_1 render 27423 4.182 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off include_2 resolve 4273 1.009 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off include_2 render 36693 5.938 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off include_3 resolve 6993 1.533 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off include_3 render 48845 7.84 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off siblings_50 resolve 5761 18.817 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off siblings_50 render 37696 25.181 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off chain_and_sibling_50 resolve 6284 26.047 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off chain_and_sibling_50 render 46965 35.482 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off deep_and_sibling_50 resolve 9004 37.965 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off deep_and_sibling_50 render 59116 42.039 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off stats_100 resolve 1175 0.192 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 off stats_100 render 18541 2.572 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on flat_10 resolve 352 0.097 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on flat_10 render 2143 0.364 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on flat_100 resolve 1251 0.231 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on flat_100 render 18613 2.579 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on sparse_100 resolve 1269 0.235 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on sparse_100 render 17233 2.217 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on include_1 resolve 4099 0.94 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on include_1 render 27773 4.368 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on include_2 resolve 4878 1.264 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on include_2 render 37298 6.226 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on include_3 resolve 7853 1.917 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on include_3 render 49705 8.099 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on siblings_50 resolve 6364 12.64 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on siblings_50 render 38299 19.024 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on chain_and_sibling_50 resolve 7142 19.128 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on chain_and_sibling_50 render 47823 25.883 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on deep_and_sibling_50 resolve 10117 27.437 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on deep_and_sibling_50 render 60229 34.508 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on stats_100 resolve 1270 0.236 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.12.2 4.0 on stats_100 render 18636 2.635 Apple M1 Max/10 067b3c7ddd98 2b9685be19cf +v1.13.0 4.0 off flat_10 resolve 257 0.054 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off flat_10 render 2048 0.315 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off flat_100 resolve 1156 0.187 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off flat_100 render 18518 2.567 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off sparse_100 resolve 1174 0.192 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off sparse_100 render 17138 2.183 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off include_1 resolve 3749 0.788 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off include_1 render 27423 4.187 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off include_2 resolve 4273 1.01 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off include_2 render 36693 5.883 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off include_3 resolve 6993 1.532 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off include_3 render 48845 7.645 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off siblings_50 resolve 5761 18.637 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off siblings_50 render 37696 24.816 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off chain_and_sibling_50 resolve 6284 27.477 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off chain_and_sibling_50 render 46965 33.07 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off deep_and_sibling_50 resolve 9004 35.44 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off deep_and_sibling_50 render 59116 42.495 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off stats_100 resolve 1175 0.192 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 off stats_100 render 18541 2.552 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on flat_10 resolve 352 0.097 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on flat_10 render 2143 0.365 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on flat_100 resolve 1251 0.231 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on flat_100 render 18613 2.601 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on sparse_100 resolve 1269 0.235 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on sparse_100 render 17233 2.229 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on include_1 resolve 4099 0.938 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on include_1 render 27773 4.39 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on include_2 resolve 4878 1.27 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on include_2 render 37298 6.207 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on include_3 resolve 7853 1.905 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on include_3 render 49705 8.284 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on siblings_50 resolve 6364 12.37 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on siblings_50 render 38299 19.827 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on chain_and_sibling_50 resolve 7142 19.951 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on chain_and_sibling_50 render 47823 26.076 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on deep_and_sibling_50 resolve 10117 25.672 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on deep_and_sibling_50 render 60229 35.144 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on stats_100 resolve 1270 0.236 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.0 4.0 on stats_100 render 18636 2.605 Apple M1 Max/10 dd10eae69f99 2b9685be19cf +v1.13.1 4.0 off flat_10 resolve 257 0.054 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off flat_10 render 2048 0.315 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off flat_100 resolve 1156 0.187 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off flat_100 render 18518 2.542 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off sparse_100 resolve 1174 0.191 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off sparse_100 render 17138 2.192 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off include_1 resolve 3748 0.784 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off include_1 render 27422 4.157 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off include_2 resolve 4271 1.011 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off include_2 render 36691 5.897 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off include_3 resolve 6990 1.532 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off include_3 render 48842 7.754 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off siblings_50 resolve 5759 21.349 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off siblings_50 render 37694 25.401 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off chain_and_sibling_50 resolve 6281 26.746 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off chain_and_sibling_50 render 46962 31.899 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off deep_and_sibling_50 resolve 9000 33.836 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off deep_and_sibling_50 render 59112 42.823 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off stats_100 resolve 1175 0.193 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 off stats_100 render 18541 2.564 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on flat_10 resolve 352 0.097 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on flat_10 render 2143 0.365 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on flat_100 resolve 1251 0.23 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on flat_100 render 18613 2.586 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on sparse_100 resolve 1269 0.236 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on sparse_100 render 17233 2.213 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on include_1 resolve 4098 0.948 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on include_1 render 27772 4.364 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on include_2 resolve 4876 1.281 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on include_2 render 37296 6.188 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on include_3 resolve 7850 1.912 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on include_3 render 49702 8.19 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on siblings_50 resolve 6362 12.713 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on siblings_50 render 38297 18.942 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on chain_and_sibling_50 resolve 7139 19.258 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on chain_and_sibling_50 render 47820 26.495 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on deep_and_sibling_50 resolve 10113 25.542 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on deep_and_sibling_50 render 60225 37.149 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on stats_100 resolve 1270 0.235 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.1 4.0 on stats_100 render 18636 2.602 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off flat_10 resolve 257 0.054 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off flat_10 render 2048 0.316 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off flat_100 resolve 1156 0.188 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off flat_100 render 18518 2.566 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off sparse_100 resolve 1174 0.191 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off sparse_100 render 17138 2.167 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off include_1 resolve 3748 0.786 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off include_1 render 27422 4.223 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off include_2 resolve 4271 1.012 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off include_2 render 36691 5.976 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off include_3 resolve 6990 1.54 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off include_3 render 48842 7.637 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off siblings_50 resolve 5759 20.02 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off siblings_50 render 37694 26.185 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off chain_and_sibling_50 resolve 6281 26.477 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off chain_and_sibling_50 render 46962 34.83 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off deep_and_sibling_50 resolve 9000 31.638 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off deep_and_sibling_50 render 59112 45.745 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off stats_100 resolve 1175 0.192 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 off stats_100 render 18541 2.558 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on flat_10 resolve 352 0.098 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on flat_10 render 2143 0.368 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on flat_100 resolve 1251 0.23 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on flat_100 render 18613 2.617 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on sparse_100 resolve 1269 0.234 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on sparse_100 render 17233 2.235 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on include_1 resolve 4098 0.943 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on include_1 render 27772 4.406 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on include_2 resolve 4876 1.276 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on include_2 render 37296 6.177 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on include_3 resolve 7850 1.906 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on include_3 render 49702 8.337 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on siblings_50 resolve 6362 13.703 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on siblings_50 render 38297 19.114 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on chain_and_sibling_50 resolve 7139 18.724 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on chain_and_sibling_50 render 47820 26.038 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on deep_and_sibling_50 resolve 10113 26.198 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on deep_and_sibling_50 render 60225 37.581 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on stats_100 resolve 1270 0.236 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.2 4.0 on stats_100 render 18636 2.636 Apple M1 Max/10 b69a7191895b 2b9685be19cf +v1.13.3 4.0 off flat_10 resolve 260 0.055 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off flat_10 render 2051 0.316 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off flat_100 resolve 1159 0.189 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off flat_100 render 18521 2.561 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off sparse_100 resolve 1177 0.193 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off sparse_100 render 17141 2.209 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off include_1 resolve 3754 0.791 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off include_1 render 27428 4.197 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off include_2 resolve 4280 1.011 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off include_2 render 36700 5.898 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off include_3 resolve 7002 1.538 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off include_3 render 48854 7.827 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off siblings_50 resolve 5768 20.204 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off siblings_50 render 37703 25.278 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off chain_and_sibling_50 resolve 6293 25.722 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off chain_and_sibling_50 render 46974 32.948 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off deep_and_sibling_50 resolve 9015 32.468 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off deep_and_sibling_50 render 59127 42.92 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off stats_100 resolve 1178 0.193 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 off stats_100 render 18544 2.575 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on flat_10 resolve 355 0.098 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on flat_10 render 2146 0.365 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on flat_100 resolve 1254 0.231 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on flat_100 render 18616 2.61 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on sparse_100 resolve 1272 0.236 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on sparse_100 render 17236 2.266 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on include_1 resolve 4104 0.945 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on include_1 render 27778 4.339 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on include_2 resolve 4885 1.281 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on include_2 render 37305 6.132 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on include_3 resolve 7862 1.907 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on include_3 render 49714 8.099 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on siblings_50 resolve 6371 12.884 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on siblings_50 render 38306 18.853 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on chain_and_sibling_50 resolve 7151 19.675 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on chain_and_sibling_50 render 47832 26.881 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on deep_and_sibling_50 resolve 10128 29.247 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on deep_and_sibling_50 render 60240 36.039 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on stats_100 resolve 1273 0.237 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.3 4.0 on stats_100 render 18639 2.618 Apple M1 Max/10 c8d7e6b64ffa 2b9685be19cf +v1.13.4 4.0 off flat_10 resolve 261 0.056 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off flat_10 render 2052 0.318 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off flat_100 resolve 1160 0.19 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off flat_100 render 18522 2.568 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off sparse_100 resolve 1178 0.196 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off sparse_100 render 17142 2.17 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off include_1 resolve 3755 0.789 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off include_1 render 27429 4.208 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off include_2 resolve 4281 1.028 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off include_2 render 36701 5.896 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off include_3 resolve 7003 1.546 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off include_3 render 48855 7.658 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off siblings_50 resolve 5770 18.054 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off siblings_50 render 37705 24.325 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off chain_and_sibling_50 resolve 6295 26.224 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off chain_and_sibling_50 render 46976 33.178 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off deep_and_sibling_50 resolve 9017 34.315 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off deep_and_sibling_50 render 59129 44.457 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off stats_100 resolve 1179 0.194 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 off stats_100 render 18545 2.562 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on flat_10 resolve 354 0.099 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on flat_10 render 2145 0.367 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on flat_100 resolve 1253 0.233 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on flat_100 render 18615 2.583 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on sparse_100 resolve 1271 0.238 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on sparse_100 render 17235 2.231 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on include_1 resolve 4101 0.944 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on include_1 render 27775 4.376 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on include_2 resolve 4880 1.286 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on include_2 render 37300 6.193 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on include_3 resolve 7855 1.931 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on include_3 render 49707 8.167 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on siblings_50 resolve 6366 12.601 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on siblings_50 render 38301 19.403 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on chain_and_sibling_50 resolve 7144 19.135 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on chain_and_sibling_50 render 47825 26.478 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on deep_and_sibling_50 resolve 10119 28.591 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on deep_and_sibling_50 render 60231 35.482 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on stats_100 resolve 1272 0.238 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v1.13.4 4.0 on stats_100 render 18638 2.605 Apple M1 Max/10 6e5ee2ced8ff 2b9685be19cf +v2.0.0-beta.1 4.0 off flat_10 resolve 257 0.055 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off flat_10 render 2048 0.315 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off flat_100 resolve 1156 0.188 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off flat_100 render 18518 2.564 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off sparse_100 resolve 1174 0.192 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off sparse_100 render 17138 2.212 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off include_1 resolve 3749 0.782 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off include_1 render 27423 4.203 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off include_2 resolve 4273 1.008 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off include_2 render 36693 5.876 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off include_3 resolve 6993 1.531 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off include_3 render 48845 7.628 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off siblings_50 resolve 5761 19.058 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off siblings_50 render 37696 24.887 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off chain_and_sibling_50 resolve 6284 29.094 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off chain_and_sibling_50 render 46965 32.163 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off deep_and_sibling_50 resolve 9004 32.941 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off deep_and_sibling_50 render 59116 40.708 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off stats_100 resolve 1175 0.193 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 off stats_100 render 18541 2.551 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on flat_10 resolve 352 0.098 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on flat_10 render 2143 0.365 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on flat_100 resolve 1251 0.232 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on flat_100 render 18613 2.594 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on sparse_100 resolve 1269 0.236 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on sparse_100 render 17233 2.234 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on include_1 resolve 4099 0.952 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on include_1 render 27773 4.404 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on include_2 resolve 4878 1.277 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on include_2 render 37298 6.18 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on include_3 resolve 7853 1.912 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on include_3 render 49705 8.194 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on siblings_50 resolve 6364 12.995 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on siblings_50 render 38299 18.639 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on chain_and_sibling_50 resolve 7142 19.566 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on chain_and_sibling_50 render 47823 28.391 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on deep_and_sibling_50 resolve 10117 26.667 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on deep_and_sibling_50 render 60229 34.895 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on stats_100 resolve 1270 0.235 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.1 4.0 on stats_100 render 18636 2.611 Apple M1 Max/10 e4d13ee6784c 2b9685be19cf +v2.0.0-beta.2 4.0 off flat_10 resolve 257 0.055 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off flat_10 render 2048 0.317 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off flat_100 resolve 1156 0.188 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off flat_100 render 18518 2.559 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off sparse_100 resolve 1174 0.193 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off sparse_100 render 17138 2.22 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off include_1 resolve 3749 0.789 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off include_1 render 27423 4.199 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off include_2 resolve 4273 1.014 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off include_2 render 36693 5.877 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off include_3 resolve 6993 1.534 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off include_3 render 48845 7.783 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off siblings_50 resolve 5761 18.797 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off siblings_50 render 37696 24.129 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off chain_and_sibling_50 resolve 6284 26.439 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off chain_and_sibling_50 render 46965 34.467 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off deep_and_sibling_50 resolve 9004 37.686 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off deep_and_sibling_50 render 59116 39.972 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off stats_100 resolve 1175 0.192 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 off stats_100 render 18541 2.571 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on flat_10 resolve 352 0.097 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on flat_10 render 2143 0.367 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on flat_100 resolve 1251 0.232 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on flat_100 render 18613 2.625 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on sparse_100 resolve 1269 0.235 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on sparse_100 render 17233 2.271 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on include_1 resolve 4099 0.943 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on include_1 render 27773 4.368 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on include_2 resolve 4878 1.278 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on include_2 render 37298 6.188 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on include_3 resolve 7853 1.908 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on include_3 render 49705 8.219 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on siblings_50 resolve 6364 13.456 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on siblings_50 render 38299 19.027 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on chain_and_sibling_50 resolve 7142 19.062 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on chain_and_sibling_50 render 47823 27.247 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on deep_and_sibling_50 resolve 10117 27.04 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on deep_and_sibling_50 render 60229 36.429 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on stats_100 resolve 1270 0.236 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.2 4.0 on stats_100 render 18636 2.652 Apple M1 Max/10 fce1205935db 2b9685be19cf +v2.0.0-beta.3 4.0 off flat_10 resolve 257 0.054 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off flat_10 render 2048 0.316 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off flat_100 resolve 1156 0.187 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off flat_100 render 18518 2.567 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off sparse_100 resolve 1174 0.193 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off sparse_100 render 17138 2.234 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off include_1 resolve 3749 0.787 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off include_1 render 27423 4.184 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off include_2 resolve 4273 1.01 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off include_2 render 36693 5.915 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off include_3 resolve 6993 1.529 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off include_3 render 48845 7.637 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off siblings_50 resolve 5761 18.681 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off siblings_50 render 37696 24.77 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off chain_and_sibling_50 resolve 6284 29.284 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off chain_and_sibling_50 render 46965 33.286 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off deep_and_sibling_50 resolve 9004 34.157 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off deep_and_sibling_50 render 59116 42.413 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off stats_100 resolve 1175 0.192 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 off stats_100 render 18541 2.554 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on flat_10 resolve 352 0.097 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on flat_10 render 2143 0.364 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on flat_100 resolve 1251 0.231 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on flat_100 render 18613 2.599 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on sparse_100 resolve 1269 0.236 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on sparse_100 render 17233 2.226 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on include_1 resolve 4099 0.947 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on include_1 render 27773 4.405 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on include_2 resolve 4878 1.275 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on include_2 render 37298 6.274 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on include_3 resolve 7853 1.912 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on include_3 render 49705 8.032 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on siblings_50 resolve 6373 12.513 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on siblings_50 render 38299 18.813 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on chain_and_sibling_50 resolve 7142 18.724 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on chain_and_sibling_50 render 47823 25.958 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on deep_and_sibling_50 resolve 10117 24.939 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on deep_and_sibling_50 render 60229 36.592 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on stats_100 resolve 1270 0.236 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.3 4.0 on stats_100 render 18636 2.598 Apple M1 Max/10 7bc7483b18ce 2b9685be19cf +v2.0.0-beta.4 4.0 off flat_10 resolve 258 0.054 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off flat_10 render 2049 0.326 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off flat_100 resolve 1157 0.188 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off flat_100 render 18519 2.65 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off sparse_100 resolve 1175 0.192 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off sparse_100 render 17139 2.267 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off include_1 resolve 3750 0.789 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off include_1 render 28424 4.748 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off include_2 resolve 4274 1.013 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off include_2 render 36694 6.283 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off include_3 resolve 6994 1.533 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off include_3 render 48846 8.494 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off siblings_50 resolve 5763 19.86 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off siblings_50 render 38698 24.804 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off chain_and_sibling_50 resolve 6286 26.38 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off chain_and_sibling_50 render 46967 32.4 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off deep_and_sibling_50 resolve 9006 35.78 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off deep_and_sibling_50 render 59118 45.249 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off stats_100 resolve 1176 0.191 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 off stats_100 render 18542 2.658 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on flat_10 resolve 351 0.097 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on flat_10 render 2142 0.373 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on flat_100 resolve 1250 0.232 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on flat_100 render 18612 2.657 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on sparse_100 resolve 1268 0.235 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on sparse_100 render 17232 2.298 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on include_1 resolve 4096 0.946 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on include_1 render 28770 4.872 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on include_2 resolve 4873 1.277 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on include_2 render 37293 6.578 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on include_3 resolve 7846 1.92 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on include_3 render 49698 8.883 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on siblings_50 resolve 6359 12.669 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on siblings_50 render 39294 20.171 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on chain_and_sibling_50 resolve 7135 18.424 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on chain_and_sibling_50 render 47816 28.253 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on deep_and_sibling_50 resolve 10108 26.885 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on deep_and_sibling_50 render 60220 36.229 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on stats_100 resolve 1269 0.238 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.4 4.0 on stats_100 render 18635 2.67 Apple M1 Max/10 029fa1abaa7d 2b9685be19cf +v2.0.0-beta.5 4.0 off flat_10 resolve 258 0.055 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off flat_10 render 2049 0.324 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off flat_100 resolve 1157 0.189 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off flat_100 render 18519 2.648 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off sparse_100 resolve 1175 0.192 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off sparse_100 render 17139 2.253 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off include_1 resolve 3750 0.788 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off include_1 render 28424 4.726 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off include_2 resolve 4274 1.008 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off include_2 render 36694 6.269 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off include_3 resolve 6994 1.541 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off include_3 render 48846 8.432 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off siblings_50 resolve 5763 18.873 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off siblings_50 render 38698 25.045 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off chain_and_sibling_50 resolve 6286 25.052 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off chain_and_sibling_50 render 46967 35.43 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off deep_and_sibling_50 resolve 9006 32.613 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off deep_and_sibling_50 render 59118 39.683 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off stats_100 resolve 1176 0.193 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 off stats_100 render 18542 2.642 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on flat_10 resolve 351 0.098 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on flat_10 render 2142 0.37 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on flat_100 resolve 1250 0.231 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on flat_100 render 18612 2.65 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on sparse_100 resolve 1268 0.236 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on sparse_100 render 17232 2.292 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on include_1 resolve 4096 0.95 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on include_1 render 28770 4.891 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on include_2 resolve 4873 1.283 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on include_2 render 37293 6.632 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on include_3 resolve 7846 1.913 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on include_3 render 49698 8.692 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on siblings_50 resolve 6359 12.698 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on siblings_50 render 39294 19.043 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on chain_and_sibling_50 resolve 7135 19.792 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on chain_and_sibling_50 render 47816 26.861 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on deep_and_sibling_50 resolve 10108 26.439 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on deep_and_sibling_50 render 60220 35.659 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on stats_100 resolve 1269 0.236 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.5 4.0 on stats_100 render 18635 2.716 Apple M1 Max/10 b6a5ca21045a 2b9685be19cf +v2.0.0-beta.6 4.0 off flat_10 resolve 261 0.055 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off flat_10 render 2052 0.321 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off flat_100 resolve 1160 0.188 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off flat_100 render 18522 2.598 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off sparse_100 resolve 1178 0.193 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off sparse_100 render 17142 2.219 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off include_1 resolve 3756 0.783 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off include_1 render 28430 4.762 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off include_2 resolve 4283 1.009 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off include_2 render 36703 6.312 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off include_3 resolve 7006 1.531 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off include_3 render 48858 8.427 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off siblings_50 resolve 5772 18.173 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off siblings_50 render 38707 26.886 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off chain_and_sibling_50 resolve 6298 25.445 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off chain_and_sibling_50 render 46979 35.073 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off deep_and_sibling_50 resolve 9021 34.237 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off deep_and_sibling_50 render 59133 43.096 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off stats_100 resolve 1179 0.193 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 off stats_100 render 18545 2.618 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on flat_10 resolve 354 0.099 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on flat_10 render 2145 0.37 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on flat_100 resolve 1253 0.231 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on flat_100 render 18615 2.647 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on sparse_100 resolve 1271 0.235 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on sparse_100 render 17235 2.269 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on include_1 resolve 4102 0.943 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on include_1 render 28776 4.919 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on include_2 resolve 4882 1.28 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on include_2 render 37302 6.606 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on include_3 resolve 7858 1.904 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on include_3 render 49710 8.954 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on siblings_50 resolve 6368 13.013 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on siblings_50 render 39303 19.747 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on chain_and_sibling_50 resolve 7147 21.232 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on chain_and_sibling_50 render 47828 25.273 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on deep_and_sibling_50 resolve 10123 25.944 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on deep_and_sibling_50 render 60235 37.207 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on stats_100 resolve 1272 0.236 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.6 4.0 on stats_100 render 18638 2.649 Apple M1 Max/10 54724512be92 2b9685be19cf +v2.0.0-beta.7 4.0 off flat_10 resolve 276 0.068 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off flat_10 render 2067 0.336 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off flat_100 resolve 1265 0.284 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off flat_100 render 18627 2.688 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off sparse_100 resolve 1283 0.288 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off sparse_100 render 17247 2.325 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off include_1 resolve 4013 1.051 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off include_1 render 28687 5.055 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off include_2 resolve 4546 1.282 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off include_2 render 36966 6.575 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off include_3 resolve 7471 1.904 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off include_3 render 49323 8.803 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off siblings_50 resolve 6131 19.491 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off siblings_50 render 39066 25.942 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off chain_and_sibling_50 resolve 6663 25.56 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off chain_and_sibling_50 render 47344 34.266 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off deep_and_sibling_50 resolve 9588 33.224 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off deep_and_sibling_50 render 59700 44.36 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off stats_100 resolve 1284 0.288 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 off stats_100 render 18650 2.697 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on flat_10 resolve 369 0.11 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on flat_10 render 2160 0.383 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on flat_100 resolve 1358 0.328 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on flat_100 render 18720 2.715 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on sparse_100 resolve 1376 0.331 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on sparse_100 render 17340 2.366 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on include_1 resolve 4359 1.213 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on include_1 render 29033 5.174 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on include_2 resolve 5145 1.555 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on include_2 render 37565 6.87 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on include_3 resolve 8323 2.287 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on include_3 render 50175 9.397 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on siblings_50 resolve 6727 13.783 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on siblings_50 render 39662 19.987 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on chain_and_sibling_50 resolve 7512 19.665 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on chain_and_sibling_50 render 48193 29.046 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on deep_and_sibling_50 resolve 10690 26.463 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on deep_and_sibling_50 render 60802 36.872 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on stats_100 resolve 1377 0.333 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.7 4.0 on stats_100 render 18743 2.73 Apple M1 Max/10 dc796e84f3e3 2b9685be19cf +v2.0.0-beta.8 4.0 off flat_10 resolve 261 0.066 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off flat_10 render 2052 0.33 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off flat_100 resolve 1250 0.282 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off flat_100 render 18612 2.68 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off sparse_100 resolve 1268 0.286 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off sparse_100 render 17232 2.291 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off include_1 resolve 3981 1.049 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off include_1 render 28655 5.063 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off include_2 resolve 4497 1.278 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off include_2 render 36917 6.484 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off include_3 resolve 7405 1.903 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off include_3 render 49257 8.853 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off siblings_50 resolve 6097 20.799 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off siblings_50 render 39032 25.853 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off chain_and_sibling_50 resolve 6612 26.662 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off chain_and_sibling_50 render 47293 34.569 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off deep_and_sibling_50 resolve 9520 35.417 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off deep_and_sibling_50 render 59632 40.503 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off stats_100 resolve 1269 0.287 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 off stats_100 render 18635 2.71 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on flat_10 resolve 354 0.109 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on flat_10 render 2145 0.378 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on flat_100 resolve 1343 0.326 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on flat_100 render 18705 2.71 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on sparse_100 resolve 1361 0.329 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on sparse_100 render 17325 2.35 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on include_1 resolve 4327 1.211 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on include_1 render 29001 5.218 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on include_2 resolve 5096 1.544 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on include_2 render 37516 6.941 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on include_3 resolve 8257 2.286 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on include_3 render 50109 9.431 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on siblings_50 resolve 6693 13.095 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on siblings_50 render 39628 20.498 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on chain_and_sibling_50 resolve 7461 20.247 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on chain_and_sibling_50 render 48142 28.534 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on deep_and_sibling_50 resolve 10623 26.56 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on deep_and_sibling_50 render 60734 34.629 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on stats_100 resolve 1362 0.331 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.8 4.0 on stats_100 render 18728 2.74 Apple M1 Max/10 2165777d9e16 2b9685be19cf +v2.0.0-beta.9 4.0 off flat_10 resolve 257 0.062 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off flat_10 render 2048 0.328 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off flat_100 resolve 1156 0.234 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off flat_100 render 18518 2.644 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off sparse_100 resolve 1174 0.24 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off sparse_100 render 17138 2.284 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off include_1 resolve 3743 0.897 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off include_1 render 28417 4.963 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off include_2 resolve 4261 1.141 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off include_2 render 36681 6.532 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off include_3 resolve 6969 1.673 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off include_3 render 48821 8.556 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off siblings_50 resolve 5765 20.786 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off siblings_50 render 38700 26.764 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off chain_and_sibling_50 resolve 6282 27.754 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off chain_and_sibling_50 render 46963 34.382 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off deep_and_sibling_50 resolve 8990 35.15 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off deep_and_sibling_50 render 59102 43.963 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off stats_100 resolve 1175 0.241 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 off stats_100 render 18541 2.642 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on flat_10 resolve 257 0.063 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on flat_10 render 2048 0.328 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on flat_100 resolve 1156 0.235 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on flat_100 render 18518 2.627 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on sparse_100 resolve 1174 0.239 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on sparse_100 render 17138 2.27 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on include_1 resolve 4029 1.04 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on include_1 render 28703 4.997 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on include_2 resolve 4739 1.371 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on include_2 render 37159 6.689 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on include_3 resolve 7639 1.989 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on include_3 render 49491 9.032 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on siblings_50 resolve 6362 13.039 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on siblings_50 render 39297 19.42 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on chain_and_sibling_50 resolve 7071 19.442 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on chain_and_sibling_50 render 47752 29.828 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on deep_and_sibling_50 resolve 9971 26.146 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on deep_and_sibling_50 render 60083 38.049 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on stats_100 resolve 1175 0.241 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.9 4.0 on stats_100 render 18541 2.638 Apple M1 Max/10 8fcd0903ae12 2b9685be19cf +v2.0.0-beta.10 4.0 off flat_10 resolve 257 0.061 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off flat_10 render 2048 0.373 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off flat_100 resolve 1156 0.234 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off flat_100 render 18518 3.073 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off sparse_100 resolve 1174 0.238 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off sparse_100 render 17138 2.666 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off include_1 resolve 3743 0.906 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off include_1 render 28417 5.183 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off include_2 resolve 4261 1.138 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off include_2 render 36681 6.796 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off include_3 resolve 6969 1.676 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off include_3 render 48821 9.041 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off siblings_50 resolve 5765 20.502 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off siblings_50 render 38700 26.673 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off chain_and_sibling_50 resolve 6282 26.631 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off chain_and_sibling_50 render 46963 36.221 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off deep_and_sibling_50 resolve 8990 32.558 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off deep_and_sibling_50 render 59102 41.185 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off stats_100 resolve 1175 0.239 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 off stats_100 render 18541 3.085 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on flat_10 resolve 257 0.062 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on flat_10 render 2048 0.372 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on flat_100 resolve 1156 0.236 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on flat_100 render 18518 3.058 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on sparse_100 resolve 1174 0.239 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on sparse_100 render 17138 2.679 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on include_1 resolve 4031 1.044 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on include_1 render 28705 5.329 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on include_2 resolve 4743 1.369 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on include_2 render 37163 7.093 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on include_3 resolve 7645 1.995 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on include_3 render 49497 9.424 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on siblings_50 resolve 6366 12.783 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on siblings_50 render 39301 19.148 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on chain_and_sibling_50 resolve 7077 21.702 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on chain_and_sibling_50 render 47758 27.632 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on deep_and_sibling_50 resolve 9979 28.799 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on deep_and_sibling_50 render 60091 36.431 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on stats_100 resolve 1175 0.24 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.10 4.0 on stats_100 render 18541 3.089 Apple M1 Max/10 f5c8899f5218 2b9685be19cf +v2.0.0-beta.11 4.0 off flat_10 resolve 262 0.06 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off flat_10 render 2053 0.345 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off flat_100 resolve 1161 0.193 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off flat_100 render 18523 2.786 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off sparse_100 resolve 1179 0.197 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off sparse_100 render 17143 2.398 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off include_1 resolve 3549 0.789 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off include_1 render 28223 4.761 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off include_2 resolve 4068 1.026 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off include_2 render 36488 6.327 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off include_3 resolve 6593 1.804 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off include_3 render 48445 8.861 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off siblings_50 resolve 5519 19.193 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off siblings_50 render 38454 25.986 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off chain_and_sibling_50 resolve 6037 25.817 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off chain_and_sibling_50 render 46718 33.322 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off deep_and_sibling_50 resolve 8562 31.851 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off deep_and_sibling_50 render 58674 42.48 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off stats_100 resolve 1180 0.196 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 off stats_100 render 18546 2.798 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on flat_10 resolve 262 0.06 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on flat_10 render 2053 0.345 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on flat_100 resolve 1161 0.194 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on flat_100 render 18523 2.758 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on sparse_100 resolve 1179 0.198 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on sparse_100 render 17143 2.382 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on include_1 resolve 3549 0.791 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on include_1 render 28223 4.714 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on include_2 resolve 4068 1.025 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on include_2 render 36488 6.444 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on include_3 resolve 6593 1.804 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on include_3 render 48445 8.662 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on siblings_50 resolve 6120 13.08 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on siblings_50 render 39055 19.449 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on chain_and_sibling_50 resolve 6832 18.136 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on chain_and_sibling_50 render 47513 27.317 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on deep_and_sibling_50 resolve 9551 25.916 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on deep_and_sibling_50 render 59663 34.859 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on stats_100 resolve 1180 0.198 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.11 4.0 on stats_100 render 18546 2.836 Apple M1 Max/10 5b14956d9a6e 2b9685be19cf +v2.0.0-beta.13 4.0 off flat_10 resolve 262 0.057 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off flat_10 render 2053 0.344 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off flat_100 resolve 1161 0.191 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off flat_100 render 18523 2.784 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off sparse_100 resolve 1179 0.196 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off sparse_100 render 17143 2.373 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off include_1 resolve 3596 0.85 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off include_1 render 28470 4.86 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off include_2 resolve 4312 1.313 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off include_2 render 36932 6.774 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off include_3 resolve 6838 2.082 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off include_3 render 49090 8.881 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off siblings_50 resolve 5616 19.725 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off siblings_50 render 38751 26.203 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off chain_and_sibling_50 resolve 6331 26.913 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off chain_and_sibling_50 render 47212 34.262 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off deep_and_sibling_50 resolve 8857 33.11 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off deep_and_sibling_50 render 59369 42.982 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off stats_100 resolve 1180 0.195 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 off stats_100 render 18546 2.834 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on flat_10 resolve 262 0.058 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on flat_10 render 2053 0.345 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on flat_100 resolve 1161 0.193 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on flat_100 render 18523 2.748 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on sparse_100 resolve 1179 0.196 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on sparse_100 render 17143 2.402 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on include_1 resolve 3596 0.854 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on include_1 render 28470 4.937 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on include_2 resolve 4312 1.317 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on include_2 render 36932 6.69 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on include_3 resolve 6838 2.098 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on include_3 render 49090 9.366 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on siblings_50 resolve 6217 12.474 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on siblings_50 render 39352 18.852 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on chain_and_sibling_50 resolve 7126 20.334 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on chain_and_sibling_50 render 48007 27.688 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on deep_and_sibling_50 resolve 9846 26.046 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on deep_and_sibling_50 render 60358 36.062 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on stats_100 resolve 1180 0.197 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +v2.0.0-beta.13 4.0 on stats_100 render 18546 2.825 Apple M1 Max/10 11e7528bb9c5 2b9685be19cf +pending 4.0 off flat_10 resolve 265 0.061 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off flat_10 render 2056 0.351 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off flat_100 resolve 1164 0.195 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off flat_100 render 18526 2.795 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off sparse_100 resolve 1182 0.199 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off sparse_100 render 17146 2.406 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off include_1 resolve 3555 0.845 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off include_1 render 28229 4.891 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off include_2 resolve 4077 1.244 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off include_2 render 36497 6.501 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off include_3 resolve 6602 2.032 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off include_3 render 48454 8.772 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off siblings_50 resolve 5528 19.673 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off siblings_50 render 38463 25.533 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off chain_and_sibling_50 resolve 6049 25.232 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off chain_and_sibling_50 render 46730 33.563 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off deep_and_sibling_50 resolve 8574 32.007 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off deep_and_sibling_50 render 58686 42.615 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off stats_100 resolve 1183 0.199 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 off stats_100 render 18549 2.82 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on flat_10 resolve 265 0.062 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on flat_10 render 2056 0.35 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on flat_100 resolve 1164 0.196 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on flat_100 render 18526 2.83 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on sparse_100 resolve 1182 0.201 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on sparse_100 render 17146 2.415 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on include_1 resolve 3555 0.85 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on include_1 render 28229 4.757 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on include_2 resolve 4077 1.249 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on include_2 render 36497 6.543 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on include_3 resolve 6602 2.036 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on include_3 render 48454 9.681 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on siblings_50 resolve 6129 12.15 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on siblings_50 render 39064 19.531 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on chain_and_sibling_50 resolve 6844 18.438 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on chain_and_sibling_50 render 47525 27.101 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on deep_and_sibling_50 resolve 9563 26.157 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on deep_and_sibling_50 render 59675 36.137 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on stats_100 resolve 1183 0.2 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf +pending 4.0 on stats_100 render 18549 2.842 Apple M1 Max/10 bce6f3e3b779 2b9685be19cf diff --git a/spec/performance/scenarios.rb b/spec/performance/scenarios.rb new file mode 100644 index 00000000..579b23b5 --- /dev/null +++ b/spec/performance/scenarios.rb @@ -0,0 +1,85 @@ +# performance_history.tsv stores the id, so it must never change. The description is only displayed. +# +# latency is what every adapter call waits, standing in for a database. Only +# scenarios whose sideloads can overlap carry it, since a wait is the only thing +# the thread pool has to overlap. The rest measure graphiti's own work. +module Scenarios + # Measured once: the pool breaks even around a millisecond a query and saves a third by five. + QUERY_LATENCY = 0.005 + + ALL = { + "flat_10" => { + description: "page[size]=10", + seed: {employees: 10}, + params: {page: {size: 10}} + }, + "flat_100" => { + description: "page[size]=100", + seed: {employees: 100}, + params: {page: {size: 100}} + }, + "sparse_100" => { + description: "page[size]=100&fields[employees]=first_name", + seed: {employees: 100}, + params: {page: {size: 100}, fields: {employees: "first_name"}} + }, + "include_1" => { + description: "page[size]=50&include=positions", + seed: {employees: 50, positions_per_employee: 4, departments: 5}, + params: {page: {size: 50}, include: "positions"} + }, + "include_2" => { + description: "page[size]=50&include=positions.department", + seed: {employees: 50, positions_per_employee: 4, departments: 5}, + params: {page: {size: 50}, include: "positions.department"} + }, + "include_3" => { + description: "page[size]=50&include=positions.department.positions", + seed: {employees: 50, positions_per_employee: 4, departments: 5}, + params: {page: {size: 50}, include: "positions.department.positions"} + }, + "siblings_50" => { + description: "page[size]=50&include=positions,credit_cards", + seed: {employees: 50, positions_per_employee: 4, departments: 5, credit_cards_per_employee: 2}, + params: {page: {size: 50}, include: "positions,credit_cards"}, + latency: QUERY_LATENCY + }, + "chain_and_sibling_50" => { + description: "page[size]=50&include=positions.department,credit_cards", + seed: {employees: 50, positions_per_employee: 4, departments: 5, credit_cards_per_employee: 2}, + params: {page: {size: 50}, include: "positions.department,credit_cards"}, + latency: QUERY_LATENCY + }, + "deep_and_sibling_50" => { + description: "page[size]=50&include=positions.department.positions,credit_cards", + seed: {employees: 50, positions_per_employee: 4, departments: 5, credit_cards_per_employee: 2}, + params: {page: {size: 50}, include: "positions.department.positions,credit_cards"}, + latency: QUERY_LATENCY + }, + "stats_100" => { + description: "page[size]=100&stats[total]=count", + seed: {employees: 100}, + params: {page: {size: 100}, stats: {total: "count"}} + } + }.freeze + + PHASES = %w[resolve render].freeze + + module_function + + def describe(id) + ALL.dig(id, :description) || id + end + + def descriptions + ALL.transform_values { |scenario| scenario[:description] } + end + + def latency(id) + ALL.dig(id, :latency).to_f + end + + def waiting + ALL.select { |_, scenario| scenario[:latency].to_f.positive? }.keys + end +end diff --git a/spec/performance/vendor/chart.umd.js b/spec/performance/vendor/chart.umd.js new file mode 100644 index 00000000..d9fd6ecf --- /dev/null +++ b/spec/performance/vendor/chart.umd.js @@ -0,0 +1,14 @@ +/*! + * Chart.js v4.4.7 + * https://www.chartjs.org + * (c) 2024 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function N(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const s=e.length;let n=0,o=s;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:h,max:c,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(n=J(Math.min(it(r,l,h).lo,i?s:it(e,l,a.getPixelForValue(h)).lo),0,s-1)),o=u?J(Math.max(it(r,a.axis,c,!0).hi+1,i?0:it(e,l,a.getPixelForValue(c),!0).hi+1),n,s)-n:s-n}return{start:n,count:o}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class bt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var xt=new bt; +/*! + * @kurkle/color v0.3.2 + * https://github.com/kurkle/color#readme + * (c) 2023 Jukka Kurkela + * Released under the MIT License + */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Zt{constructor(t){if(t instanceof Zt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Zt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Jt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Jt(t)?t:new Zt(t)}function te(t){return Jt(t)?t:new Zt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function be(t,e){return me(t).getPropertyValue(e)}const xe=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=xe[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=t&&ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Math.floor(t.height*s),o=Math.floor(t.width*s);t.height=Math.floor(t.height),t.width=Math.floor(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=be(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){const o=i||t;void 0===s&&(s=ti("_fallback",t));const a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Je(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Je(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Je(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e)||"constructor"===e)return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Ze(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Je(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Ze(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e="x"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const bi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,xi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(bi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(xi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:Z,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hx||l(n,b,p)&&0!==r(n,b),v=()=>!x||0===r(o,p)||l(o,b,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==b&&(x=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,b=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r{t[a]&&t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Xi={evaluateInteractionItems:Hi,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tji(t,ve(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return Yi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>Ui(t,ve(e,t),"x",i.intersect,s),y:(t,e,i,s)=>Ui(t,ve(e,t),"y",i.intersect,s)}};const qi=["left","top","right","bottom"];function Ki(t,e){return t.filter((t=>t.pos===e))}function Gi(t,e){return t.filter((t=>-1===qi.indexOf(t.pos)&&t.box.axis===e))}function Zi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function Ji(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!qi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function ss(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Zi(Ki(e,"left"),!0),n=Zi(Ki(e,"right")),o=Zi(Ki(e,"top"),!0),a=Zi(Ki(e,"bottom")),r=Gi(e,"x"),l=Gi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ki(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);ts(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=Ji(l.concat(h),d);ss(r.fullSize,g,d,p),ss(l,g,d,p),ss(h,g,d,p)&&ss(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),os(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,os(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class rs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class ls extends rs{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const hs="$chartjs",cs={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ds=t=>null===t||""===t;const us=!!Se&&{passive:!0};function fs(t,e,i){t&&t.canvas&&t.canvas.removeEventListener(e,i,us)}function gs(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function ps(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.addedNodes,s),e=e&&!gs(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function ms(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.removedNodes,s),e=e&&!gs(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const bs=new Map;let xs=0;function _s(){const t=window.devicePixelRatio;t!==xs&&(xs=t,bs.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function ys(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){bs.size||window.addEventListener("resize",_s),bs.set(t,e)}(t,o),a}function vs(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){bs.delete(t),bs.size||window.removeEventListener("resize",_s)}(t)}function Ms(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=cs[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t&&t.addEventListener(e,i,us)}(s,e,n),n}class ws extends rs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t[hs]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ds(n)){const e=Pe(t,"width");void 0!==e&&(t.width=e)}if(ds(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[hs])return!1;const i=e[hs].initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[hs],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:ps,detach:ms,resize:ys}[e]||Ms;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:vs,detach:vs,resize:vs}[e]||fs)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=t&&ge(t);return!(!e||!e.isConnected)}}function ks(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?ls:ws}var Ss=Object.freeze({__proto__:null,BasePlatform:rs,BasicPlatform:ls,DomPlatform:ws,_detectPlatform:ks});const Ps="transparent",Ds={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Ps),n=s.valid&&Qt(e||Ps);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class Cs{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Ds[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new Cs(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(xt.add(this._chart,i),!0):void 0}}function As(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Ts(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function zs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Vs(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Bs=t=>"reset"===t||"none"===t,Ws=(t,e)=>e?t:Object.assign({},t);class Ns{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Es(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Vs(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Fs(t,"x")),o=e.yAxisID=l(i.yAxisID,Fs(t,"y")),a=e.rAxisID=l(i.rAxisID,Fs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Vs(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e)){const t=this._cachedMeta;this._data=function(t,e){const{iScale:i,vScale:s}=e,n="x"===i.axis?"x":"y",o="x"===s.axis?"x":"y",a=Object.keys(t),r=new Array(a.length);let l,h,c;for(l=0,h=a.length;l0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Ts(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Ws(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Os(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Bs(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Bs(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Bs(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function js(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for($s(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,Us=(t,e)=>Math.min(e||t,t);function Xs(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Ks(t){return t.drawTicks?t.tickLength:0}function Gs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Zs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class Js extends Hs{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=J(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Ks(t.grid)-e.padding-Gs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(J((h.highest.height+6)/o,-1,1)),Math.asin(J(a/r,-1,1))-Math.asin(J(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Gs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Ks(n)+o):(t.height=this.maxHeight,t.width=Ks(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Ks(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,b=function(t){return Ae(i,t,p)};let x,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)x=b(this.bottom),w=this.bottom-u,S=x-m,D=b(t.top)+m,O=t.bottom;else if("bottom"===a)x=b(this.top),D=t.top,O=b(t.bottom)-m,w=x+m,S=this.top+u;else if("left"===a)x=b(this.right),M=this.right-u,k=x-m,P=b(t.left)+m,C=t.right;else if("right"===a)x=b(this.left),P=t.left,C=b(t.right)-m,M=x+m,k=this.left+u;else if("x"===e){if("center"===a)x=b((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=x+m,S=w+u}else if("y"===e){if("center"===a)x=b((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}M=x-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_0&&(o-=s/2)}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}b.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return b}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return"left"===e?s?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l+=a)):(l=this.right-o,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l=this.left)):"right"===e?s?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l-=a)):(l=this.left+o,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class tn{constructor(){this.controllers=new Qs(Ns,"datasets",!0),this.elements=new Qs(Hs,"elements"),this.plugins=new Qs(Object,"plugins"),this.scales=new Qs(Js,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function nn(t,e){return e||!1!==t?!0===t?{}:t:null}function on(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function an(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function rn(t){if("x"===t||"y"===t||"r"===t)return t}function ln(t,...e){if(rn(t))return t;for(const s of e){const e=s.axis||("top"===(i=s.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&rn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function hn(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function cn(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=an(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=ln(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return hn(t,"x",i[0])||hn(t,"y",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?"_index_":"_value_"}(l,n),c=i.scales||{};a[e]=x(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||an(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),x(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];x(e,[ue.scales[e.type],ue.scale])})),a}function dn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=cn(t,e)}function un(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const fn=new Map,gn=new Set;function pn(t,e){let i=fn.get(t);return i||(i=e(),fn.set(t,i),gn.add(i)),i}const mn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class bn{constructor(t){this._config=function(t){return(t=t||{}).data=un(t.data),dn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=un(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),dn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return pn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return pn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return pn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return pn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>mn(r,t,e)))),e.forEach((t=>mn(r,s,t))),e.forEach((t=>mn(r,re[n]||{},t))),e.forEach((t=>mn(r,ue,t))),e.forEach((t=>mn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),gn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=xn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||_n(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=xn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function xn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const _n=t=>o(t)&&Object.getOwnPropertyNames(t).some((e=>S(t[e])));const yn=["top","bottom","left","right","chartArea"];function vn(t,e){return"top"===t||"bottom"===t||-1===yn.indexOf(t)&&"x"===e}function Mn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function wn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function kn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Sn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Pn={},Dn=t=>{const e=Sn(t);return Object.values(Pn).filter((t=>t.canvas===e)).pop()};function Cn(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}function On(t,e,i){return t.options.clip?t[i]:e[i]}class An{static defaults=ue;static instances=Pn;static overrides=re;static registry=en;static version="4.4.7";static getChart=Dn;static register(...t){en.add(...t),Tn()}static unregister(...t){en.remove(...t),Tn()}constructor(t,e){const s=this.config=new bn(e),n=Sn(t),o=Dn(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||ks(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new sn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Pn[this.id]=this,r&&l?(xt.listen(this,"complete",wn),xt.listen(this,"progress",kn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return en}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return xt.stop(this),this}resize(t,e){xt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=ln(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=ln(o,n),r=l(n.type,e.dtype);void 0!==n.position&&vn(n.position,a)===vn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(en.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{as.configure(this,t,t.options),as.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(Mn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{as.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){Cn(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;as.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i=t._clip,s=!i.disabled,n=function(t,e){const{xScale:i,yScale:s}=t;return i&&s?{left:On(i,e,"left"),right:On(i,e,"right"),top:On(s,e,"top"),bottom:On(s,e,"bottom")}:e}(t,this.chartArea),o={meta:t,index:t.index,cancelable:!0};!1!==this.notifyPlugins("beforeDatasetDraw",o)&&(s&&Ie(e,{left:!1===i.left?0:n.left-i.left,right:!1===i.right?this.width:n.right+i.right,top:!1===i.top?0:n.top-i.top,bottom:!1===i.bottom?this.height:n.bottom+i.bottom}),t.controller.draw(),s&&ze(e),o.cancelable=!1,this.notifyPlugins("afterDatasetDraw",o))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Xi.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),xt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Tn(){return u(An.instances,(t=>t._plugins.invalidate()))}function Ln(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class En{static override(t){Object.assign(En.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return Ln()}parse(){return Ln()}format(){return Ln()}add(){return Ln()}diff(){return Ln()}startOf(){return Ln()}endOf(){return Ln()}}var Rn={_date:En};function In(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Fn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i,color:s}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nZ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>Z(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),b=g(C,h,d),x=g(C+E,c,u);s=(p-b)/2,n=(m-x)/2,o=-(p+b)/2,a=-(m+x)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),b=(i.width-o)/f,x=(i.height-o)/g,_=Math.max(Math.min(b,x)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Yn=Object.freeze({__proto__:null,BarController:class extends Ns{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Fn(t,e,i,s)}parseArrayData(t,e,i,s){return Fn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=this._cachedMeta.controller.getParsed(e),l=r&&r[i.axis],h=t=>{const e=t._parsed.find((t=>t[i.axis]===l)),n=e&&e[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!h(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(b-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);b=Math.max(Math.min(b,h),o),d=b+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(b))}if(b===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;b+=t,u-=t}return{size:u,base:b,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,l="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=b?g:{};if(i=x){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),b||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends jn{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},PolarAreaController:$n,RadarController:class extends Ns{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>b,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),x||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Un(t,e,i,s){const n=vi(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return J(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:J(n.innerStart,0,a),innerEnd:J(n.innerEnd,0,a)}}function Xn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function qn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,b=n-p-f,{outerStart:x,outerEnd:_,innerStart:y,innerEnd:v}=Un(e,u,d,b-m),M=d-x,w=d-_,k=m+x/M,S=b-_/w,P=u+y,D=u+v,O=m+y/P,A=b-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=Xn(w,S,a,r);t.arc(e.x,e.y,_,S,b+E)}const i=Xn(D,b,a,r);if(t.lineTo(i.x,i.y),v>0){const e=Xn(D,A,a,r);t.arc(e.x,e.y,v,b+E,A+Math.PI)}const s=(b-v/u+(m+y/u))/2;if(t.arc(a,r,u,b-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=Xn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=Xn(M,m,a,r);if(t.lineTo(n.x,n.y),x>0){const e=Xn(M,k,a,r);t.arc(e.x,e.y,x,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Kn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u}=l,f="inner"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,f?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let g=e.endAngle;if(o){qn(t,e,i,s,g,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,g),o||(qn(t,e,i,s,g,n),t.stroke())}function Gn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Jn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[x(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[x(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(b*m+e)/++b):(_(),t.lineTo(e,i),u=s,b=0,f=g=i),p=i}_()}function eo(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?to:Qn}const io="function"==typeof Path2D;function so(t,e,i,s){io&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Gn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=eo(e);for(const r of n)Gn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class no extends Hs{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||"monotone"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps(["x","y"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a),g=Z(n,a,r)&&a!==r,p=f>=O||g,m=tt(o,h+u,c+u);return p&&m}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){qn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function po(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,b=!s(a),x=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!b&&!x)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),b&&x&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=b?a:M,w=x?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(b&&(u&&M!==a?(i.push({value:a}),Mr)break;i.push({value:t})}return x&&u&&w!==r?i.length&&V(i[i.length-1].value,r,mo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):x&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&j(n,this,"value"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class xo extends bo{static id="linear";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const _o=t=>Math.floor(z(t)),yo=(t,e)=>Math.pow(10,_o(t)+e);function vo(t){return 1===t/Math.pow(10,_o(t))}function Mo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function wo(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=_o(e);let o=function(t,e){let i=_o(e-t);for(;Mo(t,e,i)>10;)i++;for(;Mo(t,e,i)<10;)i--;return Math.min(i,_o(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:vo(g),significand:u}),s}class ko extends Js{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=bo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===yo(this.min,0)?yo(this.min,-1):yo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(yo(i,-1)),o(yo(s,1)))),i<=0&&n(yo(s,-1)),s<=0&&o(yo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=wo({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function So(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Po(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Do(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Oo(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(h),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function Ao(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function To(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Lo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(So(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/So(this.options))}generateTickLabels(t){bo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Do(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));To(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:"middle"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e||0===e&&this.min<0){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash||[]),o.lineDashOffset=n.dashOffset,o.beginPath(),Lo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&this.min>=0&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Ro={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Io=Object.keys(Ro);function zo(t,e){return t-e}function Fo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Vo(t,e,i,s){const n=Io.length;for(let o=Io.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function Wo(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class No extends Js{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new Rn._date(t.adapters.date);s.init(e),x(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Fo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Vo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=Io.length-1;o>=Io.indexOf(i);o--){const i=Io[o];if(Ro[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return Io[i?Io.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=Io.indexOf(t)+1,i=Io.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=J(s,0,o),n=J(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Vo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var jo=Object.freeze({__proto__:null,CategoryScale:class extends Js{static id="category";static defaults={ticks:{callback:po}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:J(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:go(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return po.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:xo,LogarithmicScale:ko,RadialLinearScale:Eo,TimeScale:No,TimeSeriesScale:class extends No{static id="timeseries";static defaults=No.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=Ho(e,this.min),this._tableRange=Ho(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(Ho(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return Ho(this._table,i*this._tableRange+this._minPos,!0)}}});const $o=["rgb(54, 162, 235)","rgb(255, 99, 132)","rgb(255, 159, 64)","rgb(255, 205, 86)","rgb(75, 192, 192)","rgb(153, 102, 255)","rgb(201, 203, 207)"],Yo=$o.map((t=>t.replace("rgb(","rgba(").replace(")",", 0.5)")));function Uo(t){return $o[t%$o.length]}function Xo(t){return Yo[t%Yo.length]}function qo(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof jn?e=function(t,e){return t.backgroundColor=t.data.map((()=>Uo(e++))),e}(i,e):n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Uo(e),t.backgroundColor=Xo(e),++e}(i,e))}}function Ko(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Go={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n,a=Ko(s)||(r=n)&&(r.borderColor||r.backgroundColor)||o&&Ko(o)||"rgba(0,0,0,0.1)"!==ue.borderColor||"rgba(0,0,0,0.1)"!==ue.backgroundColor;var r;if(!i.forceOverride&&a)return;const l=qo(t);s.forEach(l)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Jo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var Qo={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Jo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=J(it(e,o.axis,a).lo,0,i-1)),s=h?J(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const b=[],x=e+i-1,_=t[e].x,y=t[x].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&b.push({...t[e],x:p}),s!==u&&s!==i&&b.push({...t[s],x:p})}o>0&&i!==u&&b.push(t[i]),b.push(a),h=e,m=0,f=g=l,c=d=u=o}}return b}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Jo(t)}};function ta(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ea(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function ia(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function sa(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ea(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new no({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function na(t){return t&&!1!==t.fill}function oa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function aa(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function ra(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&da(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;na(i)&&da(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;na(s)&&"beforeDatasetDraw"===i.drawTime&&da(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const ba=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class xa extends Hs{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=ba(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=_a(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=ba(o,d),b=this.isHorizontal(),x=this._computeTitleHeight();f=b?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+x,line:0}:{x:this.left+c,y:ft(n,this.top+x+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),b?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+x+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,b?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),b)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=_a(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class va extends Hs{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var Ma={id:"title",_element:va,start(t,e,i){!function(t,e){const i=new va({ctx:t.ctx,options:e,chart:t});as.configure(t,i,e),as.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;as.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const wa=new WeakMap;var ka={id:"subtitle",start(t,e,i){const s=new va({ctx:t.ctx,options:i,chart:t});as.configure(t,s,i),as.addBox(t,s),wa.set(t,s)},stop(t){as.removeBox(t,wa.get(t)),wa.delete(t)},beforeUpdate(t,e,i){const s=wa.get(t);as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa={average(t){if(!t.length)return!1;let e,i,s=new Set,n=0,o=0;for(e=0,i=t.length;et+e))/s.size,y:n/o}},nearest(t,e){if(!t.length)return!1;let i,s,n,o=e.x,a=e.y,r=Number.POSITIVE_INFINITY;for(i=0,s=t.length;i-1?t.split("\n"):t}function Ca(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Oa(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,b=0,x=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-g)*l.lineHeight+(x-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){b=Math.max(b,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),b+=p.width,{width:b,height:m}}function Aa(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ta(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||Aa(t,e,i,s),yAlign:s}}function La(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:J(g,0,s.width-e.width),y:J(p,0,s.height-e.height)}}function Ea(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function Ra(t){return Pa([],Da(t))}function Ia(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const za={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Ia(i,t);Pa(e.before,Da(Fa(n,"beforeLabel",this,t))),Pa(e.lines,Fa(n,"label",this,t)),Pa(e.after,Da(Fa(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return Ra(Fa(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Fa(i,"beforeFooter",this,t),n=Fa(i,"footer",this,t),o=Fa(i,"afterFooter",this,t);let a=[];return a=Pa(a,Da(s)),a=Pa(a,Da(n)),a=Pa(a,Da(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Ia(t.callbacks,e);s.push(Fa(i,"labelColor",this,e)),n.push(Fa(i,"labelPointStyle",this,e)),o.push(Fa(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Sa[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Oa(this,i),a=Object.assign({},t,e),r=Ta(this.chart,i,a),l=La(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,b,x,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,x=_+o,y=_-o):(p=d+f,m=p+o,x=_-o,y=_+o),b=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(x=u,_=x-o,p=m-o,b=m+o):(x=u+g,_=x+o,p=m+o,b=m-o),y=x),{x1:p,x2:m,x3:b,y1:x,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ea(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let b,x,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ea(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Sa[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Oa(this,t),a=Object.assign({},i,this._size),r=Ta(e,t,a),l=La(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Sa[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Ba={id:"tooltip",_element:Va,positioners:Sa,afterInit(t,e,i){i&&(t.tooltip=new Va({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:za},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return An.register(Yn,jo,fo,t),An.helpers={...Wi},An._adapters=Rn,An.Animation=Cs,An.Animations=Os,An.animator=xt,An.controllers=en.controllers.items,An.DatasetController=Ns,An.Element=Hs,An.elements=fo,An.Interaction=Xi,An.layouts=as,An.platforms=Ss,An.Scale=Js,An.Ticks=ae,Object.assign(An,Yn,jo,fo,t,Ss),An.Chart=An,"undefined"!=typeof window&&(window.Chart=An),An})); +//# sourceMappingURL=chart.umd.js.map diff --git a/spec/persistence_spec.rb b/spec/persistence_spec.rb index fb5334ef..bfdcde01 100644 --- a/spec/persistence_spec.rb +++ b/spec/persistence_spec.rb @@ -37,6 +37,467 @@ def expect_errors(object, expected) expect(employee.data.first_name).to eq("Jane") end + it "keeps a client-supplied id on create" do + captured_attributes = nil + klass.before_attributes do |attributes| + captured_attributes = attributes.dup + end + payload[:data][:id] = "789" + + employee = klass.build(payload) + + expect(employee.data.id).to eq(789) + expect(captured_attributes[:id]).to eq(789) + end + + it "can access the unsaved model after build" do + employee = klass.build(payload) + expect(employee.data).to_not be_nil + expect(employee.data.first_name).to eq("Jane") + expect(employee.data.id).to be_nil + end + + it "can modify attributes directly on the unsaved model before save" do + employee = klass.build(payload) + expect(employee.data).to_not be_nil + employee.data.first_name = "June" + + expect(employee.save).to eq(true) + expect(employee.data.first_name).to eq("June") + end + + # Controllers use this to answer "what would happen if I saved this?" - render + # the model with the payload applied, or its validation errors, without touching + # the database. See also the "dry run" specs in spec/integration/rails. + describe "inspecting the model before saving" do + let(:callback_passes) { [] } + let(:klass) do + passes = callback_passes + Class.new(PORO::EmployeeResource) do + self.model = PORO::Employee + + before_attributes do |attributes| + passes << attributes + attributes + end + + def self.name + "PORO::EmployeeResource" + end + end + end + + describe "writable guard evaluation" do + let(:klass) do + Class.new(PORO::EmployeeResource) do + self.model = PORO::Employee + + def self.name + "PORO::EmployeeResource" + end + + class << self + attr_accessor :guard_calls + end + self.guard_calls = 0 + + attribute :first_name, :string, writable: :first_name_writable? + + def first_name_writable?(_model = nil, _attribute = nil) + self.class.guard_calls += 1 + true + end + end + end + + # Guards run once when the proxy is built and once during + # #assign_attributes. #save skips re-validating the payload it already + # validated, so inspecting the model costs no extra guard evaluations + # over the plain-save path. + it "does not re-run guards at save time" do + employee = klass.build(payload) + employee.data + calls_after_assign = klass.guard_calls + + expect(employee.save).to eq(true) + expect(klass.guard_calls).to eq(calls_after_assign) + end + + it "still runs guards at save time on the plain-save path" do + employee = klass.build(payload) + calls_before_save = klass.guard_calls + + expect(employee.save).to eq(true) + expect(klass.guard_calls).to eq(calls_before_save + 1) + end + end + + describe "on create" do + it "exposes an unsaved model without persisting it" do + employee = klass.build(payload) + + expect(employee.data.first_name).to eq("Jane") + expect(employee.data.id).to be_nil + expect(employee.data.valid?).to eq(true) + expect(PORO::DB.data[:employees]).to be_empty + end + + it "runs the attributes callbacks once, no matter how often data is read" do + employee = klass.build(payload) + + 3.times { employee.data } + employee.assign_attributes(payload) + employee.save + + expect(callback_passes.length).to eq(1) + end + + it "returns the same model instance every time" do + employee = klass.build(payload) + expect(employee.data).to equal(employee.data) + end + + it "exposes validation errors without persisting" do + klass.model = Class.new(PORO::Employee) { + validates :first_name, presence: true + + def self.name + "PORO::Employee" + end + } + payload[:data][:attributes] = {first_name: nil} + + employee = klass.build(payload) + + expect(employee.data.valid?).to eq(false) + expect_errors(employee.data, ["First name can't be blank"]) + expect(PORO::DB.data[:employees]).to be_empty + end + + it "still persists after the model has been inspected" do + employee = klass.build(payload) + employee.data + + expect(employee.save).to eq(true) + expect(employee.data.id).to_not be_nil + end + + # The persistence callbacks receive a frozen copy of the attributes + # (see Resource::Persistence#create) - the payload itself must stay + # mutable for anything reading it after save, e.g. after_commit hooks. + it "does not leave the payload attributes frozen after save" do + employee = klass.build(payload) + employee.data + employee.save + + expect(employee.payload.attributes).to_not be_frozen + end + end + + describe "on update" do + let!(:employee) { PORO::Employee.create(first_name: "asdf") } + + before do + payload[:data][:id] = employee.id.to_s + end + + it "reads the persisted model until the payload is applied" do + proxy = klass.find(payload) + expect(proxy.data.first_name).to eq("asdf") + + proxy.assign_attributes(payload) + expect(proxy.data.first_name).to eq("Jane") + end + + it "does not persist the applied payload" do + klass.find(payload).assign_attributes(payload) + + expect(PORO::Employee.find(employee.id).first_name).to eq("asdf") + end + + it "runs the attributes callbacks once across repeated calls" do + proxy = klass.find(payload) + + proxy.data + proxy.assign_attributes(payload) + proxy.assign_attributes(payload) + + expect(callback_passes.length).to eq(1) + end + + it "assigns onto the already-resolved model rather than reloading it" do + proxy = klass.find(payload) + resolved = proxy.data + + expect(proxy.assign_attributes(payload)).to equal(resolved) + end + + it "re-assigns onto the same model when called with different params" do + proxy = klass.find(payload) + first_assigned = proxy.assign_attributes(payload) + + new_payload = {data: {type: "employees", id: employee.id.to_s, attributes: {first_name: "June"}}} + second_assigned = proxy.assign_attributes(new_payload) + + expect(second_assigned).to equal(first_assigned) + expect(proxy.data.first_name).to eq("June") + expect(proxy.update_attributes).to eq(true) + expect(PORO::Employee.find(employee.id).first_name).to eq("June") + end + + it "supports Rails-style find-then-assign" do + proxy = klass.find(id: employee.id.to_s) + proxy.assign_attributes(payload) + + expect(proxy.data.first_name).to eq("Jane") + expect(PORO::Employee.find(employee.id).first_name).to eq("asdf") + end + + it "does not re-assign when only read-side query params differ" do + proxy = klass.find(payload) + proxy.assign_attributes(payload) + proxy.assign_attributes(payload.merge(sort: "-id")) + + expect(callback_passes.length).to eq(1) + end + + it "does not mutate the params passed by the caller" do + original = {data: {type: "employees", id: employee.id.to_s, attributes: {first_name: "Jill"}}} + snapshot = Marshal.load(Marshal.dump(original)) + + proxy = klass.find(id: employee.id.to_s) + proxy.assign_attributes(original) + + expect(original).to eq(snapshot) + end + + it "persists once the payload has already been applied" do + proxy = klass.find(payload) + proxy.assign_attributes(payload) + + expect(proxy.update_attributes).to eq(true) + expect(PORO::Employee.find(employee.id).first_name).to eq("Jane") + expect(callback_passes.length).to eq(1) + end + + it "keeps changes made to the model after the payload was applied" do + proxy = klass.find(payload) + proxy.assign_attributes(payload) + proxy.data.first_name = "June" + + expect(proxy.update_attributes).to eq(true) + expect(PORO::Employee.find(employee.id).first_name).to eq("June") + end + + it "can modify the model for rendering without persisting" do + proxy = klass.find(payload) + proxy.data.first_name = "June" + + expect(proxy.data.first_name).to eq("June") + expect(PORO::Employee.find(employee.id).first_name).to eq("asdf") + end + end + + # Attributes are assigned before the persistence callbacks fire, so + # around_persistence receives the assigned model - its pre-yield position + # is the last chance to modify the model before save. + describe "with an around_persistence hook" do + context "that modifies the model before yielding" do + before do + klass.class_eval do + around_persistence :do_around_persistence + + def do_around_persistence(model) + model.first_name = "hooked" + yield + end + end + end + + it "persists the pre-yield change on create" do + employee = klass.build(payload) + + expect(employee.save).to eq(true) + expect(PORO::Employee.find(employee.data.id).first_name).to eq("hooked") + end + + it "receives the same instance the caller inspected" do + employee = klass.build(payload) + inspected = employee.data + + expect(employee.save).to eq(true) + expect(employee.data).to equal(inspected) + expect(PORO::Employee.find(employee.data.id).first_name).to eq("hooked") + end + + it "assigns and saves in one call when update is given params" do + existing = PORO::Employee.create(first_name: "asdf") + payload[:data][:id] = existing.id.to_s + + proxy = klass.find(payload) + expect(proxy.update(payload)).to eq(true) + expect(PORO::Employee.find(existing.id).first_name).to eq("hooked") + end + + it "assigns and saves when the endpoint was found with a non-string id" do + existing = PORO::Employee.create(first_name: "asdf") + payload[:data][:id] = existing.id.to_s + + proxy = klass.find(id: existing.id) + expect(proxy.update(payload)).to eq(true) + expect(PORO::Employee.find(existing.id).first_name).to eq("hooked") + end + + it "persists the pre-yield change on update" do + existing = PORO::Employee.create(first_name: "asdf") + payload[:data][:id] = existing.id.to_s + + proxy = klass.find(payload) + proxy.assign_attributes(payload) + + expect(proxy.update_attributes).to eq(true) + expect(PORO::Employee.find(existing.id).first_name).to eq("hooked") + end + end + + context "that only wraps the save" do + before do + klass.class_eval do + around_persistence :do_around_persistence + + def do_around_persistence(model) + saved = yield + saved.update_attributes(first_name: "#{saved.first_name}after") + end + end + end + + it "works after the model has been inspected" do + employee = klass.build(payload) + employee.data + + expect(employee.save).to eq(true) + expect(PORO::Employee.find(employee.data.id).first_name).to eq("Janeafter") + end + end + + context "that raises FrozenError on some other object" do + before do + klass.class_eval do + around_persistence :do_around_persistence + + def do_around_persistence(attributes) + "frozen".freeze << "oops" + yield + end + end + end + + it "propagates the original error untouched" do + employee = klass.build(payload) + employee.data + + expect { + employee.save + }.to raise_error(FrozenError, /can't modify frozen String/) + end + end + + context "that raises a FrozenError constructed without a receiver" do + before do + klass.class_eval do + around_persistence :do_around_persistence + + def do_around_persistence(attributes) + raise FrozenError, "custom frozen boom" + end + end + end + + it "propagates the original error untouched" do + employee = klass.build(payload) + employee.data + + expect { + employee.save + }.to raise_error(FrozenError, "custom frozen boom") + end + + it "propagates it on the one-shot path too" do + expect { + klass.build(payload).save + }.to raise_error(FrozenError, "custom frozen boom") + end + end + end + + describe "with an overridden create/update" do + # The assigned model rides on the resource instance, so overrides pass + # it through no matter their signature. + [ + ["a one-arg override", "def create(attributes)\n super(attributes)\nend"], + ["a two-arg override", "def create(attributes, meta = nil)\n super\nend"], + ["a forwarding override", "def create(...)\n super\nend"] + ].each do |description, override| + context "with #{description}" do + before do + klass.class_eval(override) + end + + it "saves the inspected instance" do + employee = klass.build(payload) + employee.data.first_name = "June" + + expect(employee.save).to eq(true) + expect(PORO::Employee.find(employee.data.id).first_name).to eq("June") + end + + it "still works on the one-shot path" do + employee = klass.build(payload) + + expect(employee.save).to eq(true) + expect(employee.data.first_name).to eq("Jane") + end + end + end + + context "when the override reads assigned_model" do + before do + klass.class_eval do + def create(attributes) + assigned_model.last_name = "hooked" if assigned_model + super + end + end + end + + it "exposes the inspected instance" do + employee = klass.build(payload) + employee.data + + expect(employee.save).to eq(true) + expect(PORO::Employee.find(employee.data.id).last_name).to eq("hooked") + end + + it "is nil on the plain-save path" do + employee = klass.build(payload) + + expect(employee.save).to eq(true) + expect(PORO::Employee.find(employee.data.id).last_name).to_not eq("hooked") + end + end + + it "clears assigned_model after the save" do + employee = klass.build(payload) + employee.data + resource = employee.resource + + expect(employee.save).to eq(true) + expect(resource.assigned_model).to be_nil + end + end + end + describe "updating" do let!(:employee) { PORO::Employee.create(first_name: "asdf") } @@ -52,6 +513,16 @@ def expect_errors(object, expected) }.to raise_error(Graphiti::Errors::RecordNotFound) end end + + it "can apply attributes and access model" do + employee = klass.find(payload) + expect(employee.data.first_name).to eq("asdf") + employee.assign_attributes(payload) + expect(employee.data.first_name).to eq("Jane") + + employee = klass.find(payload) + expect(employee.data.first_name).to eq("asdf") + end end describe "destroying" do @@ -663,8 +1134,8 @@ def do_after_save(model) attrs[:first_name] = "#{attrs[:first_name]}mid" end - def do_around_persistence(attributes) - attributes[:first_name] = "b4" + def do_around_persistence(model) + model.first_name = "b4" model = yield model.update_attributes(first_name: "#{model.first_name}after") 1 # return value shouldnt matter @@ -687,7 +1158,7 @@ def do_around_persistence(attributes) it "can modify attributes and the saved model" do reloaded = PORO::Employee.find(employee.id) - expect(reloaded.first_name).to eq("b4midafter") + expect(reloaded.first_name).to eq("b4after") end end end @@ -697,7 +1168,7 @@ def do_around_persistence(attributes) it "can modify attributes and the saved model" do reloaded = PORO::Employee.find(employee.id) - expect(reloaded.first_name).to eq("b4midafter") + expect(reloaded.first_name).to eq("b4after") end end end @@ -727,60 +1198,6 @@ def do_around_persistence(attributes) include_examples "around persistence", only_update: true end - - describe "deprecation of pre-yield attribute mutation" do - context "when a hook modifies the attributes hash before yield" do - before do - klass.class_eval do - around_persistence :do_around_persistence - - def do_around_persistence(attributes) - attributes[:first_name] = "b4" - yield - end - end - end - - it "warns on create, naming the changed keys" do - expect(Graphiti::DEPRECATOR).to receive(:warn) - .with(/around_persistence hook modified the attributes hash before yield \(changed keys: :first_name\)/) - klass.build(payload).save - end - - it "warns on update" do - employee = PORO::Employee.create(first_name: "asdf") - payload[:data][:id] = employee.id.to_s - - expect(Graphiti::DEPRECATOR).to receive(:warn).with(/before yield/) - klass.find(payload).update_attributes - end - end - - context "when a hook only wraps its yield" do - before do - klass.class_eval do - around_persistence :do_around_persistence - - def do_around_persistence(attributes) - model = yield - model.update_attributes(first_name: "#{model.first_name}after") - end - end - end - - it "does not warn" do - expect(Graphiti::DEPRECATOR).not_to receive(:warn) - klass.build(payload).save - end - end - - context "when no around_persistence hook is registered" do - it "does not warn" do - expect(Graphiti::DEPRECATOR).not_to receive(:warn) - klass.build(payload).save - end - end - end end describe ".around_save" do @@ -1169,21 +1586,21 @@ def around_save_c(model) model.first_name << "_aroundsaveC2" end - def around_persistence_a(attrs) - attrs[:first_name] << "_aroundpersA1" - model = yield attrs + def around_persistence_a(model) + model.first_name << "_aroundpersA1" + model = yield model model.first_name << "_aroundpersA2" end - def around_persistence_b(attrs) - attrs[:first_name] << "_aroundpersB1" - model = yield attrs + def around_persistence_b(model) + model.first_name << "_aroundpersB1" + model = yield model model.first_name << "_aroundpersB2" end - def around_persistence_c(attrs) - attrs[:first_name] << "_aroundpersC1" - model = yield attrs + def around_persistence_c(model) + model.first_name << "_aroundpersC1" + model = yield model model.first_name << "_aroundpersC2" end end @@ -1194,8 +1611,6 @@ def around_persistence_c(attrs) proxy.save expect(proxy.data.first_name.split("_")).to eq([ "Jane", - "aroundpersA1", - "aroundpersB1", "aroundattrsA1", "aroundattrsB1", "b4attrsA", @@ -1204,6 +1619,8 @@ def around_persistence_c(attrs) "afterattrsB", "aroundattrsB2", "aroundattrsA2", + "aroundpersA1", + "aroundpersB1", "aroundsaveA1", "aroundsaveB1", "b4saveA", @@ -1224,9 +1641,6 @@ def around_persistence_c(attrs) proxy.update_attributes expect(proxy.data.first_name.split("_")).to eq([ "Jane", - "aroundpersA1", - "aroundpersB1", - "aroundpersC1", "aroundattrsA1", "aroundattrsB1", "aroundattrsC1", @@ -1239,6 +1653,9 @@ def around_persistence_c(attrs) "aroundattrsC2", "aroundattrsB2", "aroundattrsA2", + "aroundpersA1", + "aroundpersB1", + "aroundpersC1", "aroundsaveA1", "aroundsaveB1", "aroundsaveC1", @@ -1274,7 +1691,7 @@ def around_persistence_c(attrs) relationships: { positions: { data: [{ - type: "positions", 'temp-id': "abc123", method: "create" + type: "positions", "temp-id": "abc123", method: "create" }] } } @@ -1282,7 +1699,7 @@ def around_persistence_c(attrs) included: [ { type: "positions", - 'temp-id': "abc123", + "temp-id": "abc123", attributes: {title: "Engineer"} } ] @@ -1879,8 +2296,8 @@ def delete(model, meta) context "and it is a create operation" do it "works" do - instance = klass.build(payload) expect { + instance = klass.build(payload) instance.save }.to raise_error(Graphiti::Errors::InvalidRequest, /data.attributes.id/) end @@ -1989,11 +2406,11 @@ def save(value) end it "coerces integers" do - expect(save(1)).to eq(BigDecimal("1")) + expect(save(1)).to eq(BigDecimal(1)) end it "coerces strings" do - expect(save("1")).to eq(BigDecimal("1")) + expect(save("1")).to eq(BigDecimal(1)) end it "allows nils" do @@ -2310,7 +2727,7 @@ def save(value) positions: { data: [{ type: "positions", - 'temp-id': "abc123", + "temp-id": "abc123", method: "create" }] } @@ -2319,7 +2736,7 @@ def save(value) included: [ { type: "positions", - 'temp-id': "abc123", + "temp-id": "abc123", attributes: {title: "mytitle"} } ] @@ -2387,7 +2804,7 @@ def self.name classification: { data: { type: "classifications", - 'temp-id': "abc123", + "temp-id": "abc123", method: "create" } } @@ -2395,7 +2812,7 @@ def self.name }, included: [ { - 'temp-id': "abc123", + "temp-id": "abc123", type: "classifications", attributes: {description: "classy"} } @@ -2440,6 +2857,16 @@ def self.name expect(data.classification.description).to eq("classy") end + it "works when the model is inspected before save" do + employee = klass.build(payload) + employee.data # force assignment + expect(employee.save).to eq(true) + data = employee.data + expect(data.id).to be_present + expect(data.classification).to be_a(classification_model) + expect(data.classification_id).to eq(data.classification.id) + end + context "when a nested validation error" do before do payload[:included][0].delete(:attributes) @@ -2513,7 +2940,7 @@ def self.name bio: { data: { type: "bios", - 'temp-id': "abc123", + "temp-id": "abc123", method: "create" } } @@ -2522,7 +2949,7 @@ def self.name included: [ { type: "bios", - 'temp-id': "abc123", + "temp-id": "abc123", attributes: {text: "mytext"} } ] @@ -2589,7 +3016,7 @@ def self.name teams: { data: [{ type: "teams", - 'temp-id': "abc123", + "temp-id": "abc123", method: "create" }] } @@ -2598,7 +3025,7 @@ def self.name included: [ { type: "teams", - 'temp-id': "abc123", + "temp-id": "abc123", attributes: {name: "ip"} } ] @@ -2666,7 +3093,7 @@ def self.name credit_card: { data: { type: jsonapi_type, - 'temp-id': "abc123", + "temp-id": "abc123", method: "create" } } @@ -2674,7 +3101,7 @@ def self.name }, included: [ { - 'temp-id': "abc123", + "temp-id": "abc123", type: jsonapi_type, attributes: {number: 123456} } @@ -2746,7 +3173,7 @@ def self.name positions: { data: [{ type: "positions", - 'temp-id': "abc123", + "temp-id": "abc123", method: "create" }] } @@ -2755,13 +3182,13 @@ def self.name included: [ { type: "positions", - 'temp-id': "abc123", + "temp-id": "abc123", attributes: {title: "mytitle"}, relationships: { department: { data: { type: "departments", - 'temp-id': "abc456", + "temp-id": "abc456", method: "create" } } @@ -2769,7 +3196,7 @@ def self.name }, { type: "departments", - 'temp-id': "abc456", + "temp-id": "abc456", attributes: {name: "mydept"} } ] diff --git a/spec/polymorphism_spec.rb b/spec/polymorphism_spec.rb index 8be69924..fcae37fa 100644 --- a/spec/polymorphism_spec.rb +++ b/spec/polymorphism_spec.rb @@ -142,8 +142,7 @@ it "does not render the relationship when it does not pertain" do render - commercials = json["data"][1]["relationships"]["commercials"] - expect(commercials["meta"]["included"]).to eq(false) + expect(json["data"][1]["relationships"]).to_not have_key("commercials") end end @@ -214,7 +213,7 @@ params[:fields] = {"commercials.actors" => "last_name"} json = JSON.parse(proxy.to_json) expect(json["data"][0]).to_not have_key("commercials") - commercials = (json["data"][1]["commercials"]) + commercials = json["data"][1]["commercials"] expect(commercials[0]["actors"]).to eq([ {"id" => actor1.id.to_s, "last_name" => "Doe"}, {"id" => actor2.id.to_s, "last_name" => "DoReMe"} @@ -228,7 +227,7 @@ } } json = JSON.parse(proxy.to_json) - commercials = (json["data"][1]["commercials"]) + commercials = json["data"][1]["commercials"] expect(commercials[0]["actors"]).to eq([{ "id" => actor2.id.to_s, "first_name" => "John", @@ -239,7 +238,7 @@ it "can sort sideloads off of type-specific ones" do params[:sort] = "-on__mastercards--commercials.actors.first_name" json = JSON.parse(proxy.to_json) - commercials = (json["data"][1]["commercials"]) + commercials = json["data"][1]["commercials"] expect(commercials[0]["actors"]).to eq([ { "id" => actor2.id.to_s, @@ -263,7 +262,7 @@ "on__mastercards--commercials.actors.number": 2 } json = JSON.parse(proxy.to_json) - commercials = (json["data"][0]["commercials"]) + commercials = json["data"][0]["commercials"] expect(commercials[0]["actors"]).to eq([ { "id" => actor2.id.to_s, diff --git a/spec/query_spec.rb b/spec/query_spec.rb index 85c3d279..0d0645ad 100644 --- a/spec/query_spec.rb +++ b/spec/query_spec.rb @@ -290,7 +290,7 @@ context "via relationship name dot syntax" do before do - params[:filter] = {'positions.title': {eq: "asdf"}} + params[:filter] = {"positions.title": {eq: "asdf"}} end let(:expected) do @@ -312,7 +312,7 @@ context "when multiple levels" do before do - params[:filter] = {'positions.department.name': {eq: "asdf"}} + params[:filter] = {"positions.department.name": {eq: "asdf"}} end let(:expected) do @@ -608,10 +608,10 @@ before do params[:page] = { number: 2, size: 1, - 'positions.size': 2, - 'positions.number': 3, - 'positions.department.size': 3, - 'positions.department.number': 4 + "positions.size": 2, + "positions.number": 3, + "positions.department.size": 3, + "positions.department.number": 4 } end @@ -897,8 +897,10 @@ end end - describe "#links?" do - subject { instance.links? } + describe "#render_link?" do + subject { instance.render_link?(mode) } + + let(:mode) { true } it { is_expected.to eq(true) } @@ -918,16 +920,14 @@ it { is_expected.to eq(false) } end - context "when links_on_demand" do - around do |e| - original = Graphiti.config.links_on_demand - begin - Graphiti.config.links_on_demand = true - e.run - ensure - Graphiti.config.links_on_demand = original - end - end + context "when mode is false" do + let(:mode) { false } + + it { is_expected.to eq(false) } + end + + context "when mode is :on_demand" do + let(:mode) { :on_demand } context "and requested" do context "as string" do @@ -940,7 +940,7 @@ context "as boolean" do before do - params[:links] = "true" + params[:links] = true end it { is_expected.to eq(true) } @@ -953,12 +953,23 @@ end end + describe "deprecated positional arguments" do + it "warns and maps them to keywords" do + expect(Graphiti::DEPRECATOR).to receive(:warn).once.and_return(nil) + + query = described_class.new(resource, params, :positions, nil, [], :create) + + expect(query.association_name).to eq(:positions) + expect(query.action).to eq(:create) + end + end + describe "#action" do subject { instance.action } let(:provided_action) { :create } context "when provided explicitly" do - let(:instance) { described_class.new(resource, params, nil, nil, [], provided_action) } + let(:instance) { described_class.new(resource, params, action: provided_action) } it { is_expected.to eq(provided_action) } context "and the action provided is show" do @@ -1015,32 +1026,38 @@ def resource_class_of_remote_sideload(sideloads) end describe "#pagination_links?" do - subject { instance.pagination_links? } - let(:pagination_links) { Graphiti.config.pagination_links } - let(:pagination_links_on_demand) { Graphiti.config.pagination_links_on_demand } - - around do |e| - original_pagination_links = Graphiti.config.pagination_links - original_pagination_links_on_demand = Graphiti.config.pagination_links_on_demand - Graphiti.config.pagination_links = pagination_links - Graphiti.config.pagination_links_on_demand = pagination_links_on_demand - begin - e.run - ensure - Graphiti.config.pagination_links = original_pagination_links - Graphiti.config.pagination_links_on_demand = original_pagination_links_on_demand - end + it "is the deprecated name for #page_links?" do + employee_resource.page_links = true + expect(instance.pagination_links?).to eq(true) end + end - context "when pagination_links_on_demand" do - let(:pagination_links_on_demand) { true } + describe "#page_links?" do + subject { instance.page_links? } + + context "when page_links is :on_demand" do + before do + employee_resource.page_links = :on_demand + end context "when params ask for pagination" do + let(:params) { {page_links: true} } + + it { is_expected.to eq(true) } + end + + context "when params ask with the deprecated param name" do let(:params) { {pagination_links: true} } it { is_expected.to eq(true) } end + context "when params ask as a string" do + let(:params) { {page_links: "true"} } + + it { is_expected.to eq(true) } + end + context "when params dont ask pagination" do it { is_expected.to eq(false) } end @@ -1048,13 +1065,15 @@ def resource_class_of_remote_sideload(sideloads) context "when action is equal to find" do let(:params) { {action: "show"} } - let(:pagination_links_on_demand) { false } it { is_expected.to eq(false) } - context "when pagination_links_on_demand and param is present" do + context "when pagination_links is :on_demand and param is present" do let(:params) { {action: "show", pagination_links: true} } - let(:pagination_links_on_demand) { true } + + before do + employee_resource.page_links = :on_demand + end it { is_expected.to eq(false) } end @@ -1062,17 +1081,16 @@ def resource_class_of_remote_sideload(sideloads) context "when action is equal to all" do let(:params) { {action: "index"} } - let(:pagination_links_on_demand) { false } - context "when is equal config.pagination_links is true" do - let(:pagination_links) { true } + context "when pagination_links is true" do + before do + employee_resource.page_links = true + end it { is_expected.to eq(true) } end - context "when is equal config.pagination_links is false" do - let(:pagination_links) { false } - + context "when pagination_links is false" do it { is_expected.to eq(false) } end end diff --git a/spec/rails_spec_helper.rb b/spec/rails_spec_helper.rb index 32adbe04..b672b58f 100644 --- a/spec/rails_spec_helper.rb +++ b/spec/rails_spec_helper.rb @@ -11,13 +11,6 @@ require "graphiti/rails" -# graphiti-rails has own Railtie -begin - require "graphiti-rails" -rescue LoadError - require "graphiti/railtie" -end - module BasicRailsApp module_function @@ -35,20 +28,7 @@ def generate config.logger.level = Logger::DEBUG Rails.application.routes.default_url_options = {host: "example.com"} - if Rails::VERSION::MAJOR >= 6 - Rails.application.config.hosts << "www.example.com" - end - - # fix railties 5.2.0 issue with secret_key_base - # https://github.com/rails/rails/commit/7419a4f9 should take care of it - # in the future. - if Rails::VERSION::MAJOR == 5 - if Rails::VERSION::MINOR >= 2 - def secret_key_base - "3b7cd727ee24e8444053437c36cc66c4" - end - end - end + Rails.application.config.hosts << "www.example.com" } @app.respond_to?(:secrets) && @app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4" @@ -61,8 +41,7 @@ def secret_key_base class ApplicationController < ActionController::Base include Rails.application.routes.url_helpers - # FIXME: Don't always include - include Graphiti::Rails + include Graphiti::Rails::Controller end require "rspec/rails" @@ -77,20 +56,3 @@ class ApplicationController < ActionController::Base end end end - -# Get Rails 4 and ruby 2.6 working in CI -# https://github.com/rails/rails/issues/34790 -if RUBY_VERSION >= "2.6.0" - if Rails.version < "5" - class ActionController::TestResponse < ActionDispatch::TestResponse - def recycle! - # hack to avoid MonitorMixin double-initialize error: - @mon_mutex_owner_object_id = nil - @mon_mutex = nil - initialize - end - end - else - puts "Monkeypatch for ActionController::TestResponse no longer needed" - end -end diff --git a/spec/relationship_identifier_spec.rb b/spec/relationship_identifier_spec.rb index e9d9c1e8..42b32dd9 100644 --- a/spec/relationship_identifier_spec.rb +++ b/spec/relationship_identifier_spec.rb @@ -50,20 +50,22 @@ def self.name it "does not include anything" do expect do - included("employees") - end.to raise_error(GraphitiSpecHelpers::Errors::NoSideloads) + jsonapi_included("employees") + end.to raise_error(Graphiti::SpecHelpers::Errors::NoSideloads) end - it "specifies meta[:included] = false" do + it "does not render the relationship at all" do jsonapi_data.each do |record| - expect(record.relationships["employees"]["meta"]["included"]).to eq(false) + expect(record.relationships).to_not have_key("employees") end end - it "does not includes relationship identifiers" do + it "renders meta[:included] = false when placeholders are on" do + resource.relationship_placeholders = true + render + jsonapi_data.each do |record| - data = record.relationships["employees"]["data"] - expect(data).to be_nil + expect(record.relationships["employees"]).to eq("meta" => {"included" => false}) end end end @@ -92,7 +94,7 @@ def self.name end it "includes employees" do - expect(included("employees").map(&:id)).to eq([1, 2]) + expect(jsonapi_included("employees").map(&:id)).to eq([1, 2]) end it "includes relationship identifiers" do @@ -105,14 +107,14 @@ def self.name end end - context "without include directive and always_include_resource_ids: true" do + context "without include directive and resource_ids: true" do let(:resource) do Class.new(PORO::TeamResource) do def self.name "PORO::TeamResource" end - has_many :employees, always_include_resource_ids: true do + has_many :employees, resource_ids: true do scope do |employee_ids| { type: :employees, @@ -130,8 +132,8 @@ def self.name it "does not include anything" do expect do - included("employees") - end.to raise_error(GraphitiSpecHelpers::Errors::NoSideloads) + jsonapi_included("employees") + end.to raise_error(Graphiti::SpecHelpers::Errors::NoSideloads) end it "includes relationship identifiers" do @@ -162,7 +164,7 @@ def self.name end it "works" do - expect(included("employees").map(&:id)).to eq([1]) + expect(jsonapi_included("employees").map(&:id)).to eq([1]) end it "has relationship identifiers" do @@ -191,8 +193,7 @@ def self.name render end - # Currently disabled as causes an N+1 - xit "has relationship ids" do + it "has relationship ids" do jsonapi_data.each do |record| data = record.relationships["employee"]["data"] @@ -202,14 +203,46 @@ def self.name end end - context "with always_include_resource_ids: false" do + context "with a custom primary_key" do + let!(:named_employee) { PORO::Employee.create(first_name: "Steve") } + let!(:named_position) { PORO::Position.create(employee_id: "Steve") } + + let(:resource) do + Class.new(PORO::PositionResource) do + def self.name + "PORO::PositionResource" + end + + belongs_to :employee, primary_key: :first_name + end + end + + it "has no relationship identifiers, rather than the foreign key" do + render + + record = jsonapi_data.find { |node| node.id == named_position.id } + expect(record.relationships).to_not have_key("employee") + end + + it "still renders the related id when the relationship is included" do + params[:include] = "employee" + render + + record = jsonapi_data.find { |node| node.id == named_position.id } + data = record.relationships["employee"]["data"] + + expect(data[:id]).to eq(named_employee.id.to_s) + end + end + + context "with resource_ids: false" do let(:resource) do Class.new(PORO::PositionResource) do def self.name "PORO::PositionResource" end - belongs_to :employee, always_include_resource_ids: false do + belongs_to :employee, resource_ids: false do scope do |employee_ids| { type: :employees, @@ -227,10 +260,354 @@ def self.name it "has no relationship identifiers" do jsonapi_data.each do |record| - data = record.relationships["employee"] - expect(data.keys).to_not include("data") + expect(record.relationships).to_not have_key("employee") + end + end + end + + context "with belongs_to_resource_ids_by_default = :always" do + let(:resource) do + Class.new(PORO::PositionResource) do + def self.name + "PORO::PositionResource" + end + + self.belongs_to_resource_ids_by_default = :always + + belongs_to :employee do + scope do |employee_ids| + { + type: :employees, + conditions: {id: employee_ids} + } + end + end + end + end + + before do + allow_any_instance_of(PORO::Position).to receive(:employee) { employee } + render + end + + it "includes relationship identifiers without the per-relationship option" do + jsonapi_data.each do |record| + data = record.relationships["employee"]["data"] + + expect(data[:type]).to eq("employees") + expect(data[:id]).to eq("1") + end + end + + context "and the relationship opts out" do + let(:resource) do + Class.new(PORO::PositionResource) do + def self.name + "PORO::PositionResource" + end + + self.belongs_to_resource_ids_by_default = :always + + belongs_to :employee, resource_ids: false do + scope do |employee_ids| + { + type: :employees, + conditions: {id: employee_ids} + } + end + end + end + end + + it "honors the relationship over the default" do + jsonapi_data.each do |record| + expect(record.relationships).to_not have_key("employee") + end + end + end + end + + context "when the relationship is unreadable" do + let(:resource) do + Class.new(PORO::PositionResource) do + def self.name + "PORO::PositionResource" + end + + belongs_to :employee, readable: false + end + end + + it "renders no resource ids, and no relationship at all" do + render + + jsonapi_data.each do |record| + expect(record.relationships).to_not have_key("employee") + end + end + + it "does not treat the foreign key as a source for them" do + expect(resource.sideloads[:employee].render_resource_ids?).to eq(false) + end + end + + context "when the relationship is guarded" do + let(:resource) do + Class.new(PORO::PositionResource) do + def self.name + "PORO::PositionResource" + end + + belongs_to :employee, readable: :admin? + + def admin? + context.current_user == "admin" + end + end + end + + # Schema generation asks every relationship whether it renders resource + # ids, with no request to evaluate the guard against. + it "answers without running the guard" do + expect { + Graphiti.with_context(nil) do + expect(resource.sideloads[:employee].render_resource_ids?).to eq(true) + end + }.to_not raise_error + end + + it "still omits the relationship when the guard denies at render time" do + Graphiti.with_context(OpenStruct.new(current_user: "basic")) do + render + end + + jsonapi_data.each do |record| + expect(record.relationships).to_not have_key("employee") + end + end + end + + context "with belongs_to_resource_ids_by_default = :never" do + let(:resource) do + Class.new(PORO::PositionResource) do + def self.name + "PORO::PositionResource" + end + + self.belongs_to_resource_ids_by_default = :never + + belongs_to :employee + end + end + + it "renders no resource ids" do + render + + jsonapi_data.each do |record| + expect(record.relationships).to_not have_key("employee") + end + end + + it "still renders them when the request includes the relationship" do + params[:include] = "employee" + render + + jsonapi_data.each do |record| + expect(record.relationships["employee"]["data"][:id]).to eq("1") + end + end + end + + context "with belongs_to_resource_ids_by_default = :always" do + context "and the relationship is unreadable" do + let(:resource) do + Class.new(PORO::PositionResource) do + def self.name + "PORO::PositionResource" + end + + self.belongs_to_resource_ids_by_default = :always + + belongs_to :employee, readable: false + end + end + + # Advertising linkage the render can never emit would put schema.json + # permanently at odds with the payload. + it "does not claim resource ids for a relationship that renders nothing" do + expect(resource.sideloads[:employee].render_resource_ids?).to eq(false) + + render + jsonapi_data.each do |record| + expect(record.relationships).to_not have_key("employee") + end + end + end + + context "and the relationship is a polymorphic_belongs_to" do + let(:visa_resource) { PORO::VisaResource } + + let(:resource) do + visa = PORO::VisaResource + Class.new(PORO::EmployeeResource) do + def self.name + "PORO::EmployeeResource" + end + + self.belongs_to_resource_ids_by_default = :always + + polymorphic_belongs_to :credit_card do + group_by(:credit_card_type) do + on(:Visa).belongs_to :visa, resource: visa + end + end + end + end + + it "is covered, since it is a belongs_to" do + expect(resource.sideloads[:credit_card].render_resource_ids?).to eq(true) + end + end + end + + context "when belongs_to_resource_ids_by_default is unset" do + it "is :foreign_key, leaving the decision to the key itself" do + expect(PORO::PositionResource.belongs_to_resource_ids_by_default).to eq(:foreign_key) + end + end + + context "when belongs_to_resource_ids_by_default is nil" do + it "is rejected rather than treated as unspecified" do + expect { + Class.new(PORO::PositionResource) { self.belongs_to_resource_ids_by_default = nil } + }.to raise_error(Graphiti::Errors::InvalidBelongsToResourceIds) + end + end + + context "when belongs_to_resource_ids_by_default is given something else" do + it "rejects a boolean rather than guessing which state it meant" do + expect { + Class.new(PORO::PositionResource) { self.belongs_to_resource_ids_by_default = true } + }.to raise_error( + Graphiti::Errors::InvalidBelongsToResourceIds, + /must be one of :foreign_key, :always, or :never/ + ) + end + end + end + + describe "the 2.x names" do + def silenced + Graphiti::DEPRECATOR.silence { yield } + end + + it "translates the relationship option, which meant the same thing" do + resource = silenced do + Class.new(PORO::TeamResource) do + has_many :employees, always_include_resource_ids: true + end + end + + expect(resource.sideloads[:employees].render_resource_ids?).to eq(true) + end + + it "warns when the relationship option is used" do + expect(Graphiti::DEPRECATOR).to receive(:deprecation_warning) + .with(:always_include_resource_ids, /Use :resource_ids instead/) + + Class.new(PORO::TeamResource) do + has_many :employees, always_include_resource_ids: true + end + end + + it "lets the new option win when both are passed" do + resource = silenced do + Class.new(PORO::TeamResource) do + has_many :employees, always_include_resource_ids: true, resource_ids: false + end + end + + expect(resource.sideloads[:employees].render_resource_ids?).to eq(false) + end + end + + describe "a relationship the model has no method for" do + let(:resource) do + Class.new(PORO::TeamResource) do + def self.name + "PORO::TeamResource" + end + + has_many :employees, resource_ids: true + end + end + + it "names the resource and the relationship" do + expect { render }.to raise_error( + Graphiti::Errors::MissingRelationshipMethod, + /PORO::TeamResource: relationship :employees is declared, but PORO::Team has no #employees method/ + ) + end + + it "explains that resource_ids is what makes every render read the association" do + expect { render }.to raise_error( + Graphiti::Errors::MissingRelationshipMethod, + /resource_ids is set on this relationship, so every render reads the association/ + ) + end + + context "when the association exists but raises NoMethodError itself" do + before do + allow_any_instance_of(PORO::Team).to receive(:employees) do + nil.some_undefined_method end end + + it "lets the original error through" do + expect { render }.to raise_error(NoMethodError, /some_undefined_method/) + end + end + + # #receiver raises rather than returning nil for one of these, so the + # guard cannot reach for it before knowing the error came from a call. + context "when the association raises a hand-built NoMethodError naming itself" do + before do + allow_any_instance_of(PORO::Team).to receive(:employees) do + raise NoMethodError.new("custom boom", :employees) + end + end + + it "lets the original error through" do + expect { render }.to raise_error(NoMethodError, /custom boom/) + end + end + + context "when the association method exists but is private" do + let(:resource) do + Class.new(PORO::TeamResource) do + def self.name + "PORO::TeamResource" + end + + has_many :employees, resource_ids: true + end + end + + before do + PORO::Team.class_eval do + private def employees + [] + end + end + end + + after do + PORO::Team.send(:remove_method, :employees) + end + + it "does not claim the method is missing" do + expect { render }.to raise_error(NoMethodError, /private method/) + end end end end diff --git a/spec/relationship_linkage_spec.rb b/spec/relationship_linkage_spec.rb new file mode 100644 index 00000000..549b850c --- /dev/null +++ b/spec/relationship_linkage_spec.rb @@ -0,0 +1,65 @@ +require "spec_helper" + +RSpec.describe "linkage with a colliding relationship name" do + let!(:classification_resource) do + Class.new(PORO::ApplicationResource) do + def self.name + "LkClassificationResource" + end + self.model = PORO::Classification + self.type = :classifications + attribute :description, :string + end + end + + let!(:position_resource) do + cls = classification_resource + Class.new(PORO::ApplicationResource) do + def self.name + "LkPositionResource" + end + self.model = PORO::Position + self.type = :positions + attribute :employee_id, :integer, only: [:filterable] + attribute :classification_id, :integer, only: [:filterable] + belongs_to :classification, resource: cls, foreign_key: :classification_id + end + end + + let!(:employee_resource) do + cls = classification_resource + pos = position_resource + Class.new(PORO::ApplicationResource) do + def self.name + "LkEmployeeResource" + end + self.model = PORO::Employee + self.type = :employees + attribute :first_name, :string + attribute :classification_id, :integer, only: [:filterable] + belongs_to :classification, resource: cls, foreign_key: :classification_id + has_many :positions, resource: pos, foreign_key: :employee_id + end + end + + before do + PORO::Position.class_eval { attr_accessor :classification_id, :classification } + PORO::DB.clear + PORO::Classification.create(description: "c1") + employee = PORO::Employee.create(first_name: "A", classification_id: 1) + PORO::Position.create(employee_id: employee.id, classification_id: 1) + end + + def employee_classification(params) + json = JSON.parse(employee_resource.all(params).to_jsonapi) + json["data"][0]["relationships"]["classification"]["data"] + end + + it "renders the linkage from the foreign key" do + expect(employee_classification(include: "positions")).to eq({"type" => "classifications", "id" => "1"}) + end + + it "still renders it when a nested include shares the relationship name" do + expect(employee_classification(include: "positions.classification")).to eq({"type" => "classifications", "id" => "1"}) + end +end diff --git a/spec/relationship_override_spec.rb b/spec/relationship_override_spec.rb new file mode 100644 index 00000000..7696e095 --- /dev/null +++ b/spec/relationship_override_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "overriding an inherited relationship" do + include_context "resource testing" + + let(:other_resource) do + Class.new(PORO::DepartmentResource) do + def self.name + "PORO::OtherDepartmentResource" + end + + self.model = PORO::Department + self.type = :other_departments + end + end + + # PORO::PositionResource already declares `belongs_to :department` + let(:resource) do + other = other_resource + Class.new(PORO::PositionResource) do + def self.name + "PORO::PositionResource" + end + + belongs_to :department, resource: other + end + end + let(:base_scope) { {type: :positions} } + + let!(:department) { PORO::Department.create(name: "Engineering") } + let!(:position) { PORO::Position.create(title: "Developer", department_id: department.id) } + + it "renders the subclass's resource, not the one it inherited" do + render + + expect(jsonapi_data[0].relationships["department"]["data"]["type"]) + .to eq("other_departments") + end + + it "leaves the parent's serializer alone" do + resource # declare the override + + expect(PORO::PositionResource.serializer.relationship_sideloads[:department]) + .to eq(PORO::PositionResource.sideloads[:department]) + end + + it "does not replace a relationship block the application wrote by hand" do + serializer = Class.new(Graphiti::Serializer) do + relationship :department do + data { nil } + end + end + hand_written = serializer.relationship_blocks[:department] + + klass = Class.new(PORO::PositionResource) do + def self.name + "PORO::PositionResource" + end + end + klass.serializer = serializer + klass.belongs_to :department, resource: PORO::DepartmentResource + + expect(klass.serializer.relationship_blocks[:department]).to equal(hand_written) + end +end diff --git a/spec/remote_resource_spec.rb b/spec/remote_resource_spec.rb index 565ae16c..7aab1f34 100644 --- a/spec/remote_resource_spec.rb +++ b/spec/remote_resource_spec.rb @@ -403,7 +403,7 @@ def request_headers context "and there is nested pagination" do before do - params[:page] = {'positions.size': 5, 'positions.number': 2} + params[:page] = {"positions.size": 5, "positions.number": 2} end it "is passed to the remote API" do @@ -413,7 +413,7 @@ def request_headers context "and there are nested filters" do before do - params[:filter] = {'positions.title': {suffix: "a"}} + params[:filter] = {"positions.title": {suffix: "a"}} end it "is passed to the remote API" do @@ -512,8 +512,8 @@ def assert_params(params) context "when pagination params" do before do params[:page] = { - 'positions.department.size': 3, - 'positions.department.teams.size': 2 + "positions.department.size": 3, + "positions.department.teams.size": 2 } end @@ -525,8 +525,8 @@ def assert_params(params) context "when filter params" do before do params[:filter] = { - 'positions.department.name': "foo", - 'positions.department.teams.id': "4" + "positions.department.name": "foo", + "positions.department.teams.id": "4" } end @@ -640,7 +640,7 @@ def assert_remote_url(url) context "and there is a nested filter" do before do params[:include] = "positions.department" - params[:filter] = {'positions.department.name': "foo"} + params[:filter] = {"positions.department.name": "foo"} end it "queries correctly" do @@ -652,7 +652,7 @@ def assert_remote_url(url) context "and there is nested pagination" do before do params[:include] = "positions.department" - params[:page] = {'positions.department.size': 2} + params[:page] = {"positions.department.size": 2} end it "queries correctly" do diff --git a/spec/rendering_spec.rb b/spec/rendering_spec.rb index 5f6ed247..40fe1bbe 100644 --- a/spec/rendering_spec.rb +++ b/spec/rendering_spec.rb @@ -271,7 +271,7 @@ def resolve(scope) context "via dot syntax" do before do params[:include] = "positions.department.positions.department" - params[:fields] = {'positions.department.positions.department': "description"} + params[:fields] = {"positions.department.positions.department": "description"} end it "works" do diff --git a/spec/request_validator_spec.rb b/spec/request_validator_spec.rb index 524741d5..067664f3 100644 --- a/spec/request_validator_spec.rb +++ b/spec/request_validator_spec.rb @@ -70,6 +70,32 @@ def self.name end end + context "when data is not an object" do + let(:payload) { {data: [{type: "employees"}]} } + + it "adds a data error" do + expect(validate).to eq false + expect(instance.errors).to be_added(:data, :invalid) + expect(instance.errors.full_messages).to eq ["data must be an object"] + end + + it "raises InvalidRequest when using validate!" do + expect { + instance.validate! + }.to raise_error(Graphiti::Errors::InvalidRequest, /data must be an object/) + end + + context "when updating" do + let(:action) { :update } + let(:payload) { {data: [{type: "employees", id: 1}], filter: {id: 1}} } + + it "adds a data error instead of raising TypeError" do + expect(validate).to eq false + expect(instance.errors.full_messages).to eq ["data must be an object"] + end + end + end + context "when missing type" do let(:payload) do { @@ -83,7 +109,7 @@ def self.name it "adds corresponding error" do expect(validate).to eq(false) - expect(instance.errors).to be_added(:'data.type', :missing) + expect(instance.errors).to be_added(:"data.type", :missing) end end @@ -112,7 +138,7 @@ def self.name it "has an unknown attribute error" do expect(validate).to eq false - expect(instance.errors).to be_added(:'data.attributes.something_wrong', :unknown_attribute) + expect(instance.errors).to be_added(:"data.attributes.something_wrong", :unknown_attribute) end end @@ -124,7 +150,7 @@ def self.name it "has an unwritable attribute error" do expect(validate).to eq false - expect(instance.errors).to be_added(:'data.attributes.created_at', :unwritable_attribute) + expect(instance.errors).to be_added(:"data.attributes.created_at", :unwritable_attribute) end end @@ -141,7 +167,7 @@ def self.name it "has an unwritable attribute error" do expect(validate).to eq false - expect(instance.errors).to be_added(:'data.attributes.salary', :unwritable_attribute) + expect(instance.errors).to be_added(:"data.attributes.salary", :unwritable_attribute) end end @@ -190,7 +216,7 @@ def salary_writable?(model) it "has an unwritable attribute error" do expect(validate).to eq false expect(instance.errors) - .to be_added(:'data.attributes.salary', :unwritable_attribute) + .to be_added(:"data.attributes.salary", :unwritable_attribute) end end end @@ -227,7 +253,7 @@ def salary_writable?(model, attribute_name) it "is still applied" do expect(validate).to eq false expect(instance.errors) - .to be_added(:'data.attributes.salary', :unwritable_attribute) + .to be_added(:"data.attributes.salary", :unwritable_attribute) end end @@ -241,7 +267,7 @@ def salary_writable?(model, attribute_name) it "is still applied" do expect(validate).to eq false expect(instance.errors) - .to be_added(:'data.attributes.salary', :unwritable_attribute) + .to be_added(:"data.attributes.salary", :unwritable_attribute) end end end @@ -255,7 +281,7 @@ def salary_writable?(model, attribute_name) it "has a attribute access error" do expect(validate).to eq false - expect(instance.errors).to be_added(:'data.attributes.age', :type_error) + expect(instance.errors).to be_added(:"data.attributes.age", :type_error) expect(instance.errors.full_messages).to eq(["data.attributes.age should be type integer"]) end end @@ -325,16 +351,16 @@ def salary_writable?(model, attribute_name) { data: { type: "employees", - 'temp-id': "23498s", + "temp-id": "23498s", attributes: {}, relationships: { positions: { data: [{ - 'temp-id': "abc123", + "temp-id": "abc123", type: "positions", method: "create" }, { - 'temp-id': "def456", + "temp-id": "def456", type: "positions", method: "create" }] @@ -343,14 +369,14 @@ def salary_writable?(model, attribute_name) }, included: [ { - 'temp-id': "abc123", + "temp-id": "abc123", type: "positions", attributes: { title: "foo" } }, { - 'temp-id': "def456", + "temp-id": "def456", type: "positions", attributes: { title: "bar" @@ -374,11 +400,11 @@ def salary_writable?(model, attribute_name) payload[:data][:relationships][:positions_2] = { data: [{ - 'temp-id': "abc123", + "temp-id": "abc123", type: "positions", method: "create" }, { - 'temp-id': "def456", + "temp-id": "def456", type: "positions", method: "create" }] @@ -389,8 +415,8 @@ def salary_writable?(model, attribute_name) expect(validate).to eq false expect(instance.errors.count).to eq 2 - expect(instance.errors).to be_added(:'data.relationships.positions', :unwritable_relationship) - expect(instance.errors).to be_added(:'data.relationships.positions_2', :unwritable_relationship) + expect(instance.errors).to be_added(:"data.relationships.positions", :unwritable_relationship) + expect(instance.errors).to be_added(:"data.relationships.positions_2", :unwritable_relationship) end end @@ -402,7 +428,7 @@ def salary_writable?(model, attribute_name) it "includes the error" do expect(validate).to eq false - expect(instance.errors).to be_added(:'included.0.attributes.something', :unknown_attribute) + expect(instance.errors).to be_added(:"included.0.attributes.something", :unknown_attribute) end context "there are bad attributes in multiple items" do @@ -415,9 +441,9 @@ def salary_writable?(model, attribute_name) expect(validate).to eq false expect(instance.errors.count).to eq 3 - expect(instance.errors).to be_added(:'data.attributes.something', :unknown_attribute) - expect(instance.errors).to be_added(:'included.0.attributes.something', :unknown_attribute) - expect(instance.errors).to be_added(:'included.1.attributes.something', :unknown_attribute) + expect(instance.errors).to be_added(:"data.attributes.something", :unknown_attribute) + expect(instance.errors).to be_added(:"included.0.attributes.something", :unknown_attribute) + expect(instance.errors).to be_added(:"included.1.attributes.something", :unknown_attribute) end end end @@ -447,7 +473,7 @@ def salary_writable?(model, attribute_name) it "has an unknown relationship error" do expect(validate).to eq false - expect(instance.errors).to be_added(:'data.relationships.pets', :invalid_relationship) + expect(instance.errors).to be_added(:"data.relationships.pets", :invalid_relationship) end it "raises InvalidRequest when using validate!" do @@ -470,7 +496,7 @@ def salary_writable?(model, attribute_name) relationships: { positions: { data: [{ - 'temp-id': "abc123", + "temp-id": "abc123", type: "positions", method: "create" }] @@ -479,7 +505,7 @@ def salary_writable?(model, attribute_name) }, included: [ { - 'temp-id': "abc123", + "temp-id": "abc123", type: "positions", attributes: {title: "foo"} } diff --git a/spec/resource_proxy_spec.rb b/spec/resource_proxy_spec.rb index 5917bef5..f001be27 100644 --- a/spec/resource_proxy_spec.rb +++ b/spec/resource_proxy_spec.rb @@ -14,7 +14,7 @@ let(:query) { double(cache_key: "query-hash") } let(:scope) { double(cache_key: "scope-hash", cache_key_with_version: "scope-hash-123456") } - subject { described_class.new(resource, scope, query, **{cache_tag: :cache_tag}) } + subject { described_class.new(resource, scope, query, cache_tag: :cache_tag) } it "cache_key combines query and scope cache keys if no tags are set" do cache_key = subject.cache_key diff --git a/spec/resource_spec.rb b/spec/resource_spec.rb index 191d9511..a450bc86 100644 --- a/spec/resource_spec.rb +++ b/spec/resource_spec.rb @@ -16,7 +16,7 @@ expect(klass.adapter.ancestors[0]) .to eq(Graphiti::Adapters::Abstract) expect(klass.default_sort).to be_nil - expect(klass.default_page_size).to be_nil + expect(klass.page_default_size).to be_nil expect(klass.attributes_readable_by_default).to eq(true) expect(klass.attributes_writable_by_default).to eq(true) expect(klass.attributes_sortable_by_default).to eq(true) @@ -24,8 +24,8 @@ expect(klass.attributes_schema_by_default).to eq(true) expect(klass.relationships_readable_by_default).to eq(true) expect(klass.relationships_writable_by_default).to eq(true) - expect(klass.filters_accept_nil_by_default).to eq(false) - expect(klass.filters_deny_empty_by_default).to eq(false) + expect(klass.relationship_placeholders).to eq(false) + expect(klass.filter_blanks_treated_as).to eq(:literal) end it "does not have serializer, type, model, or graphql_entrypoint" do @@ -40,6 +40,41 @@ end end + describe "an abstract resource declaring relationships" do + let(:app_resource) do + Class.new(described_class) do + self.abstract_class = true + has_many :positions, resource: PORO::PositionResource + end + end + + it "does not raise" do + expect { app_resource }.to_not raise_error + expect(app_resource.serializer).to be_nil + end + + it "applies the relationship to a subclass's serializer" do + klass = Class.new(app_resource) + expect(klass.serializer.relationship_blocks.keys).to include(:positions) + end + + context "when rails eager loading" do + before do + rails = double \ + application: double(config: double(eager_load: true)) + stub_const("Rails", rails.as_null_object) + Graphiti.instance_variable_set(:@setup, false) + end + + it "applies to the subclass's serializer via setup!" do + klass = Class.new(app_resource) + expect(klass.serializer.relationship_blocks).to be_blank + expect { Graphiti.setup! }.to_not raise_error + expect(klass.serializer.relationship_blocks.keys).to include(:positions) + end + end + end + describe "a further descendant of ApplicationResource" do let(:app_resource) do Class.new(described_class) do @@ -63,7 +98,7 @@ expect(klass.adapter.ancestors[0]) .to eq(Graphiti::Adapters::Abstract) expect(klass.default_sort).to be_nil - expect(klass.default_page_size).to be_nil + expect(klass.page_default_size).to be_nil expect(klass.attributes_readable_by_default).to eq(true) expect(klass.attributes_writable_by_default).to eq(true) expect(klass.attributes_sortable_by_default).to eq(true) @@ -71,8 +106,7 @@ expect(klass.attributes_schema_by_default).to eq(true) expect(klass.relationships_readable_by_default).to eq(true) expect(klass.relationships_writable_by_default).to eq(true) - expect(klass.filters_accept_nil_by_default).to eq(false) - expect(klass.filters_deny_empty_by_default).to eq(false) + expect(klass.filter_blanks_treated_as).to eq(:literal) end context "when rails" do @@ -85,12 +119,22 @@ context "and eager loading" do let(:eager_load) { true } + before do + Graphiti.instance_variable_set(:@setup, false) + end + it "does NOT assign relationships to the serializer" do klass.has_many :positions, resource: PORO::PositionResource expect(klass.serializer.relationship_blocks).to be_blank Graphiti.setup! expect(klass.serializer.relationship_blocks).to_not be_blank end + + it "assigns relationships at definition once setup! has run" do + Graphiti.setup! + klass.has_many :positions, resource: PORO::PositionResource + expect(klass.serializer.relationship_blocks).to_not be_blank + end end context "and NOT eager loading" do @@ -150,7 +194,7 @@ def self.name Class.new(app_resource) do self.adapter = PORO::Adapter self.default_sort = [{name: :asc}] - self.default_page_size = 4 + self.page_default_size = 4 self.attributes_readable_by_default = false self.attributes_writable_by_default = false self.attributes_sortable_by_default = false @@ -158,15 +202,14 @@ def self.name self.attributes_schema_by_default = false self.relationships_readable_by_default = false self.relationships_writable_by_default = false - self.filters_accept_nil_by_default = true - self.filters_deny_empty_by_default = true + self.filter_blanks_treated_as = :rejected end end it "works" do expect(klass.adapter).to eq(PORO::Adapter) expect(klass.default_sort).to eq([{name: :asc}]) - expect(klass.default_page_size).to eq(4) + expect(klass.page_default_size).to eq(4) expect(klass.attributes_readable_by_default).to eq(false) expect(klass.attributes_writable_by_default).to eq(false) expect(klass.attributes_sortable_by_default).to eq(false) @@ -174,8 +217,7 @@ def self.name expect(klass.attributes_schema_by_default).to eq(false) expect(klass.relationships_readable_by_default).to eq(false) expect(klass.relationships_writable_by_default).to eq(false) - expect(klass.filters_accept_nil_by_default).to eq(true) - expect(klass.filters_deny_empty_by_default).to eq(true) + expect(klass.filter_blanks_treated_as).to eq(:rejected) end end @@ -309,7 +351,7 @@ class TestResourceOverrideSerializer < PORO::ApplicationSerializer Class.new(app_resource) do self.adapter = PORO::Adapter self.default_sort = [{name: :asc}] - self.default_page_size = 4 + self.page_default_size = 4 self.attributes_readable_by_default = false self.attributes_writable_by_default = false self.attributes_sortable_by_default = false @@ -317,15 +359,14 @@ class TestResourceOverrideSerializer < PORO::ApplicationSerializer self.attributes_schema_by_default = false self.relationships_readable_by_default = false self.relationships_writable_by_default = false - self.filters_accept_nil_by_default = true - self.filters_deny_empty_by_default = true + self.filter_blanks_treated_as = :rejected end end it "carries them over to the subclass" do expect(klass2.adapter).to eq(PORO::Adapter) expect(klass2.default_sort).to eq([{name: :asc}]) - expect(klass2.default_page_size).to eq(4) + expect(klass2.page_default_size).to eq(4) expect(klass2.attributes_readable_by_default).to eq(false) expect(klass2.attributes_writable_by_default).to eq(false) expect(klass2.attributes_sortable_by_default).to eq(false) @@ -333,8 +374,7 @@ class TestResourceOverrideSerializer < PORO::ApplicationSerializer expect(klass2.attributes_schema_by_default).to eq(false) expect(klass2.relationships_readable_by_default).to eq(false) expect(klass2.relationships_writable_by_default).to eq(false) - expect(klass2.filters_accept_nil_by_default).to eq(true) - expect(klass2.filters_deny_empty_by_default).to eq(true) + expect(klass2.filter_blanks_treated_as).to eq(:rejected) end end @@ -416,10 +456,10 @@ class TestResourceOverrideSerializer < PORO::ApplicationSerializer dbl = double instance.with_context(dbl, :index) do expect(instance.context).to eq(dbl) - expect(instance.context_namespace).to eq(:index) + expect(instance.current_action).to eq(:index) end expect(instance.context).to be_nil - expect(instance.context_namespace).to be_nil + expect(instance.current_action).to be_nil end context "when an error" do @@ -436,7 +476,17 @@ class TestResourceOverrideSerializer < PORO::ApplicationSerializer end }.to raise_error("foo") expect(instance.context).to eq("orig") - expect(instance.context_namespace).to eq("orig namespace") + expect(instance.current_action).to eq("orig namespace") + end + end + end + + describe "#context_namespace" do + it "is a deprecated alias of #current_action" do + instance.with_context(double, :index) do + expect(Graphiti::DEPRECATOR).to receive(:deprecation_warning) + .with(:context_namespace, /current_action/) + expect(instance.context_namespace).to eq(:index) end end end @@ -447,15 +497,116 @@ class TestResourceOverrideSerializer < PORO::ApplicationSerializer end end - describe "#default_page_size" do + describe "#page_default_size" do it "defaults" do - expect(instance.default_page_size).to be_nil + expect(instance.page_default_size).to be_nil end end - describe "#max_page_size" do + describe "#page_max_size" do it "defaults" do - expect(instance.max_page_size).to eq(1_000) + expect(instance.page_max_size).to eq(1_000) + end + end + + describe "the deprecated page setting names" do + it "reads and writes through to the page_ family" do + klass.default_page_size = 5 + klass.max_page_size = 50 + klass.cursor_paginatable = true + + expect(klass.page_default_size).to eq(5) + expect(klass.page_max_size).to eq(50) + expect(klass.page_cursors).to eq(true) + expect(klass.cursor_paginatable?).to eq(true) + end + end + + describe "the deprecated filter blank settings" do + it "maps accept_nil onto the blanks modes" do + klass.filters_accept_nil_by_default = true + expect(klass.filter_blanks_treated_as).to eq(:null) + expect(klass.filters_accept_nil_by_default).to eq(true) + + klass.filters_accept_nil_by_default = false + expect(klass.filter_blanks_treated_as).to eq(:literal) + end + + it "maps deny_empty onto the blanks modes" do + klass.filters_deny_empty_by_default = true + expect(klass.filter_blanks_treated_as).to eq(:rejected) + expect(klass.filters_deny_empty_by_default).to eq(true) + + klass.filters_deny_empty_by_default = false + expect(klass.filter_blanks_treated_as).to eq(:literal) + end + end + + describe "#filter_blanks_treated_as" do + it "rejects other values" do + expect { + klass.filter_blanks_treated_as = :nope + }.to raise_error(Graphiti::Errors::InvalidFilterBlanks, /must be one of :literal, :null, or :rejected/) + end + end + + describe "#page_links" do + it "defaults" do + expect(instance.page_links).to eq(false) + end + + it "accepts true, false, and :on_demand" do + klass.page_links = :on_demand + expect(klass.page_links).to eq(:on_demand) + end + + it "rejects other values" do + expect { + klass.page_links = :always + }.to raise_error(Graphiti::Errors::InvalidLinkRendering, /page_links must be true, false, or :on_demand/) + end + end + + describe "#typecast_reads" do + it "defaults" do + expect(instance.typecast_reads).to eq(true) + end + end + + describe "#relationship_links" do + it "inherits the base setting" do + expect(instance.relationship_links).to eq(Graphiti::Resource.relationship_links) + end + + it "is inherited" do + klass.relationship_links = :on_demand + expect(Class.new(klass).relationship_links).to eq(:on_demand) + end + + it "rejects other values" do + expect { + klass.relationship_links = :never + }.to raise_error(Graphiti::Errors::InvalidLinkRendering, /relationship_links must be true, false, or :on_demand/) + end + end + + describe "the deprecated autolink setting" do + it "reads and writes through to relationship_links" do + klass.autolink = false + expect(klass.relationship_links).to eq(false) + expect(klass.autolink?).to eq(false) + + klass.relationship_links = :on_demand + expect(klass.autolink).to eq(true) + end + + it "leaves an on-demand mode alone" do + klass.relationship_links = :on_demand + klass.autolink = true + expect(klass.relationship_links).to eq(:on_demand) + + klass.autolink = false + expect(klass.relationship_links).to eq(false) end end @@ -963,9 +1114,9 @@ def self.name it "infers from name" do expect(klass.endpoint).to eq({ - path: :'/employees', - full_path: :'/employees', - url: :'/employees', + path: :"/employees", + full_path: :"/employees", + url: :"/employees", actions: [:index, :show, :create, :update, :destroy] }) end @@ -976,11 +1127,11 @@ def self.name end it "is applied to url" do - expect(klass.endpoint[:url]).to eq(:'http://example.com/employees') + expect(klass.endpoint[:url]).to eq(:"http://example.com/employees") end it "is NOT applied to full_path" do - expect(klass.endpoint[:full_path]).to eq(:'/employees') + expect(klass.endpoint[:full_path]).to eq(:"/employees") end end @@ -990,11 +1141,11 @@ def self.name end it "is applied to full_path" do - expect(klass.endpoint[:full_path]).to eq(:'/api/v1/employees') + expect(klass.endpoint[:full_path]).to eq(:"/api/v1/employees") end it "is applied to url" do - expect(klass.endpoint[:url]).to eq(:'/api/v1/employees') + expect(klass.endpoint[:url]).to eq(:"/api/v1/employees") end end @@ -1009,9 +1160,9 @@ def self.name it "adds to path" do expect(klass.endpoint).to eq({ - path: :'/poro/employees', - full_path: :'/poro/employees', - url: :'/poro/employees', + path: :"/poro/employees", + full_path: :"/poro/employees", + url: :"/poro/employees", actions: [:index, :show, :create, :update, :destroy] }) end @@ -1028,7 +1179,7 @@ def self.name end it "infers path from name" do - expect(instance.endpoint[:path]).to eq(:'/employees') + expect(instance.endpoint[:path]).to eq(:"/employees") end it "defaults actions" do @@ -1047,7 +1198,7 @@ def self.name end it "infers path from module + name" do - expect(instance.endpoint[:path]).to eq(:'/poro/employees') + expect(instance.endpoint[:path]).to eq(:"/poro/employees") end end end @@ -1089,6 +1240,28 @@ def self.name end end + describe ".validate_endpoints" do + let(:klass) { Class.new(Graphiti::Resource) } + + it "defaults both halves on" do + expect(klass.validate_requests).to eq(true) + expect(klass.validate_links).to eq(true) + expect(klass.validate_endpoints).to eq(true) + end + + it "sets both halves" do + klass.validate_endpoints = false + expect(klass.validate_requests).to eq(false) + expect(klass.validate_links).to eq(false) + end + + it "reads false once either half is off" do + klass.validate_links = false + expect(klass.validate_endpoints).to eq(false) + expect(klass.validate_requests).to eq(true) + end + end + describe "#base_scope" do let(:adapter) do Class.new(Graphiti::Adapters::Null) do @@ -1280,8 +1453,8 @@ def self.name klass.all }.to raise_error(Graphiti::Errors::InvalidEndpoint) { |error| expect(error.message).to include("QueryAllSpec::EmployeeResource cannot be called directly from endpoint /api/v1/employees") - expect(error.message).to include("self.validate_endpoints = false") - expect(error.message).to include("https://www.graphiti.dev/guides/concepts/links") + expect(error.message).to include("self.validate_requests = false") + expect(error.message).to include("https://graphiti.dev/concepts/links") } end end @@ -1330,14 +1503,14 @@ def self.allow_request?(path, params, action) end end - context "but endpoint checks are turned off" do + context "but request checks are turned off" do around do |e| - orig = klass.validate_endpoints + orig = klass.validate_requests begin - klass.validate_endpoints = false + klass.validate_requests = false e.run ensure - klass.validate_endpoints = orig + klass.validate_requests = orig end end diff --git a/spec/runner_spec.rb b/spec/runner_spec.rb index b484468c..5e1b653b 100644 --- a/spec/runner_spec.rb +++ b/spec/runner_spec.rb @@ -37,16 +37,28 @@ subject(:query) { proxy.query } context "when an action is provided" do - let(:instance) { described_class.new(resource_class, params, nil, :action) } + let(:instance) { described_class.new(resource_class, params, action: :action) } it { expect(query.action).to eq(:action) } end context "when a query is provided" do let(:provided_query) { double(:provided_query) } - let(:instance) { described_class.new(resource_class, params, provided_query) } + let(:instance) { described_class.new(resource_class, params, query: provided_query) } it { is_expected.to eq(provided_query) } end + + context "when query and action are provided positionally" do + let(:provided_query) { double(:provided_query) } + + it "warns and maps them to keywords" do + expect(Graphiti::DEPRECATOR).to receive(:warn).once.and_return(nil) + + runner = described_class.new(resource_class, params, provided_query, :action) + + expect(runner.query).to eq(provided_query) + end + end end end end diff --git a/spec/schema_diff_spec.rb b/spec/schema_diff_spec.rb index 33963ffa..6dbfc53e 100644 --- a/spec/schema_diff_spec.rb +++ b/spec/schema_diff_spec.rb @@ -504,7 +504,7 @@ def self.name context "when default page size is added" do before do - resource_b.default_page_size = 10 + resource_b.page_default_size = 10 end it "returns error" do @@ -516,8 +516,8 @@ def self.name context "when default page size is removed" do before do - resource_a.default_page_size = 30 - resource_b.default_page_size = nil + resource_a.page_default_size = 30 + resource_b.page_default_size = nil end it "returns error" do @@ -529,8 +529,8 @@ def self.name context "when default page size changes" do before do - resource_a.default_page_size = 30 - resource_b.default_page_size = 10 + resource_a.page_default_size = 30 + resource_b.page_default_size = 10 end it "returns error" do @@ -1029,6 +1029,34 @@ def self.name it { is_expected.to eq([]) } end + context "when relationship stops including resource linkage" do + before do + resource_a.has_many :positions, + resource_ids: true, + resource: position_resource + resource_b.has_many :positions, + resource: position_resource + end + + it "returns error" do + expect(diff).to eq([ + "SchemaDiff::EmployeeResource: relationship :positions no longer includes resource linkage." + ]) + end + end + + context "when relationship starts including resource linkage" do + before do + resource_a.has_many :positions, + resource: position_resource + resource_b.has_many :positions, + resource_ids: true, + resource: position_resource + end + + it { is_expected.to eq([]) } + end + context "when relationship changes resource" do let(:position_resource2) do Class.new(application_resource) do diff --git a/spec/schema_spec.rb b/spec/schema_spec.rb index a86efbf4..b9be5dbd 100644 --- a/spec/schema_spec.rb +++ b/spec/schema_spec.rb @@ -645,7 +645,7 @@ def self.name context "when a default page size" do before do - employee_resource.default_page_size = 10 + employee_resource.page_default_size = 10 end it "is present in the resource schema" do @@ -798,7 +798,7 @@ def self.name it "generates correctly" do expect(schema[:endpoints]).to eq({ - '/api/v1/schema/employees': { + "/api/v1/schema/employees": { actions: { index: {resource: "Schema::EmployeeResource"}, show: {resource: "Schema::EmployeeResource"}, @@ -807,7 +807,7 @@ def self.name destroy: {resource: "Schema::EmployeeResource"} } }, - '/api/v1/special_employees': { + "/api/v1/special_employees": { actions: { index: {resource: "Schema::EmployeeResource"} } @@ -837,7 +837,7 @@ def self.name end it "does not add the action to the schema" do - expect(schema[:endpoints][:'/api/v1/schema/employees'][:actions].keys) + expect(schema[:endpoints][:"/api/v1/schema/employees"][:actions].keys) .to eq([:show, :create]) end end @@ -903,7 +903,6 @@ def self.name before do stub_const("Rails", rails) - allow_any_instance_of(Graphiti::Sideload).to receive(:check!) end it "eager loads classes" do @@ -1033,5 +1032,95 @@ def self.name end end end + + context "when given a path" do + before do + allow(File).to receive(:exist?).with("/other/schema.json") { false } + allow(FileUtils).to receive(:mkdir_p).with("/other") + end + + it "writes there instead of the configured path" do + expect(File).to receive(:write) + .with("/other/schema.json", JSON.pretty_generate(new_schema)) + described_class.generate!(path: "/other/schema.json") + end + end + end + + describe ".check" do + subject(:check) { described_class.check } + + let(:schema) { {resources: [], endpoints: {}, types: {}} } + let(:committed) { JSON.pretty_generate(schema) } + + before do + allow(described_class).to receive(:generate) { schema } + allow(Graphiti.config).to receive(:schema_path) { "/schema/path/schema.json" } + allow(File).to receive(:exist?).with("/schema/path/schema.json") { true } + allow(File).to receive(:read).with("/schema/path/schema.json") { committed } + end + + context "when the file matches what the app generates" do + it "is ok" do + expect(check).to be_ok + expect(check.message).to eq("Schema is up to date: /schema/path/schema.json") + end + end + + context "when the file does not exist" do + before do + allow(File).to receive(:exist?).with("/schema/path/schema.json") { false } + end + + it "is missing" do + expect(check).to be_missing + expect(check).to_not be_ok + expect(check.message).to include("Schema file not found") + end + + it "does not diff" do + expect(Graphiti::SchemaDiff).to_not receive(:new) + check.missing? + end + end + + context "when the file is out of date but still compatible" do + let(:committed) { JSON.pretty_generate(endpoints: {}, types: {}, resources: []) } + + before do + allow_any_instance_of(Graphiti::SchemaDiff).to receive(:compare) { [] } + allow(described_class).to receive(:generate) { + {resources: [{name: "PostResource"}], endpoints: {}, types: {}} + } + end + + it "is stale" do + expect(check).to be_stale + expect(check).to be_compatible + expect(check).to_not be_ok + expect(check.message).to include("Schema file is outdated") + end + end + + context "when the file holds backwards-incompatibilities" do + before do + allow_any_instance_of(Graphiti::SchemaDiff).to receive(:compare) { ["some diff error"] } + end + + it "reports them" do + expect(check).to_not be_compatible + expect(check.errors).to eq(["some diff error"]) + expect(check.message).to include("some diff error") + end + end + + describe "#write!" do + it "writes the generated schema" do + expect(FileUtils).to receive(:mkdir_p).with("/schema/path") + expect(File).to receive(:write) + .with("/schema/path/schema.json", JSON.pretty_generate(schema)) + expect(check.write!).to eq("/schema/path/schema.json") + end + end end end diff --git a/spec/scope_spec.rb b/spec/scope_spec.rb index 9ba54907..a80ac5f0 100644 --- a/spec/scope_spec.rb +++ b/spec/scope_spec.rb @@ -8,7 +8,7 @@ let(:resource) do Class.new(PORO::EmployeeResource) { - self.default_page_size = 1 + self.page_default_size = 1 }.new end let(:results) { [] } @@ -61,11 +61,12 @@ context "when the requested sideload exists on the resource" do before do + allow(resource.class).to receive(:sideload).and_call_original allow(resource.class).to receive(:sideload).with(:positions) { sideload } end it "resolves the sideload synchronously by default" do - expect(sideload).to receive(:resolve) + expect(sideload).to receive(:sync_resolve) .with(results, query.sideloads[:positions], resource) expect(sideload).not_to receive(:future_resolve) instance.resolve @@ -74,18 +75,38 @@ context "when Graphiti.config.concurrency is true" do before { allow(Graphiti.config).to receive(:concurrency).and_return(true) } - it "resolves the sideload via a future" do - expect(sideload).to receive(:future_resolve) - .with(results, query.sideloads[:positions], resource) { Concurrent::Promises.future {} } + it "resolves one sideload inline, since nothing can run beside it" do + expect(sideload).to receive(:sync_resolve) + .with(results, query.sideloads[:positions], resource) + expect(sideload).to_not receive(:future_resolve) instance.resolve end + + context "and a second sideload can run beside it" do + # A second sideload, so the pool has something to overlap. Its scope + # block ignores the parents, which are doubles here. + let(:resource) do + Class.new(PORO::EmployeeResource) { + self.page_default_size = 1 + has_many :others, resource: PORO::PositionResource, foreign_key: :employee_id do + scope { |_ids| {type: :positions, conditions: {}} } + end + }.new + end + let(:query) { Graphiti::Query.new(resource, {include: {positions: {}, others: {}}}) } + + it "resolves it via a future" do + expect(sideload).to receive(:future_resolve) { Concurrent::Promises.future {} } + instance.resolve + end + end end context "but no parents were found" do let(:results) { [] } it "does not resolve the sideload" do - expect(sideload).to_not receive(:resolve) + expect(sideload).to_not receive(:sync_resolve) instance.resolve end end @@ -94,11 +115,23 @@ context "with concurrency" do let(:position_resource) do Class.new(PORO::PositionResource) do - self.default_page_size = 1 + self.page_default_size = 1 end.new end + # Two sideloads, because one resolves inline and never reaches the pool. + let(:resource) do + Class.new(PORO::EmployeeResource) { + self.page_default_size = 1 + has_many :others, resource: PORO::PositionResource, foreign_key: :employee_id do + scope { |_ids| {type: :positions, conditions: {}} } + end + }.new + end + let(:query) { Graphiti::Query.new(resource, {include: {positions: {}, others: {}}}) } + before do + allow(resource.class).to receive(:sideload).and_call_original allow(resource.class).to receive(:sideload).with(:positions) { sideload } allow(position_resource).to receive(:resolve) { results } allow(position_resource.adapter).to receive(:close) @@ -152,7 +185,7 @@ context "when Graphiti.config.concurrency is false" do before do allow(Graphiti.config).to receive(:concurrency).and_return(false) - allow(sideload).to receive(:resolve) do |_results, q, _parent_resource| + allow(sideload).to receive(:sync_resolve) do |_results, q, _parent_resource| described_class.new(double.as_null_object, position_resource, q).resolve end end @@ -192,7 +225,7 @@ end it "resolves the sideload" do - expect(sideload).to receive(:resolve) + expect(sideload).to receive(:sync_resolve) .with(results, query.sideloads[:positions], resource) instance.resolve_sideloads(results) end @@ -229,12 +262,12 @@ let(:params) { {include: {positions: {department: {}}}} } let(:position_resource) do Class.new(PORO::PositionResource) do - self.default_page_size = 1 + self.page_default_size = 1 end.new end let(:department_resource) do Class.new(PORO::DepartmentResource) do - self.default_page_size = 1 + self.page_default_size = 1 end.new end let(:department_sideload) { double("department", shared_remote?: false, name: :department) } @@ -260,8 +293,11 @@ expect { instance.resolve_sideloads(results) }.not_to raise_error end - it "flattens the nested sideload promises" do - expect(instance.resolve_sideloads(results)).to contain_exactly(position_results) + it "resolves every nested sideload" do + instance.resolve_sideloads(results) + + expect(position_resource).to have_received(:resolve) + expect(department_resource).to have_received(:resolve) end end @@ -312,14 +348,14 @@ before { allow(Graphiti.config).to receive(:concurrency).and_return(false) } it "does not close db connection" do - allow(sideload).to receive(:resolve) + allow(sideload).to receive(:sync_resolve) expect(resource.adapter).not_to receive(:close) instance.resolve_sideloads(results) end it "does not hand back the query's sideloads" do - allow(sideload).to receive(:resolve) + allow(sideload).to receive(:sync_resolve) expect(instance.resolve_sideloads(results)).to be_nil end @@ -327,7 +363,7 @@ it "does not clear thread locals" do Thread.current[:foo] = "bar" - allow(sideload).to receive(:resolve) + allow(sideload).to receive(:sync_resolve) instance.resolve_sideloads(results) expect(Thread.current[:foo]).to eq("bar") @@ -337,7 +373,7 @@ it "does not clear fiber locals" do Fiber[:foo] = "bar" - allow(sideload).to receive(:resolve) + allow(sideload).to receive(:sync_resolve) instance.resolve_sideloads(results) expect(Fiber[:foo]).to eq("bar") @@ -349,7 +385,7 @@ let(:results) { [] } it "does not resolve the sideload" do - expect(sideload).to_not receive(:resolve) + expect(sideload).to_not receive(:sync_resolve) instance.resolve_sideloads(results) end end @@ -357,7 +393,7 @@ context "when the first sideload errors" do context "without concurrency" do before do - allow(sideload).to receive(:resolve) { raise "danger will robinson!" } + allow(sideload).to receive(:sync_resolve) { raise "danger will robinson!" } end it "raises the error" do @@ -389,8 +425,8 @@ context "without concurrency" do before do - allow(sideload).to receive(:resolve) - allow(sideload_2).to receive(:resolve) { raise "sideload_2" } + allow(sideload).to receive(:sync_resolve) + allow(sideload_2).to receive(:sync_resolve) { raise "sideload_2" } end it "raises the error" do @@ -421,8 +457,8 @@ context "without concurrency" do before do - allow(sideload).to receive(:resolve) { raise "sideload" } - allow(sideload_2).to receive(:resolve) { raise "sideload_2" } + allow(sideload).to receive(:sync_resolve) { raise "sideload" } + allow(sideload_2).to receive(:sync_resolve) { raise "sideload_2" } end it "raises the first error" do diff --git a/spec/serialization_spec.rb b/spec/serialization_spec.rb index 2413a6d6..9b610ada 100644 --- a/spec/serialization_spec.rb +++ b/spec/serialization_spec.rb @@ -105,7 +105,7 @@ def my_method it "can call methods on the resource" do define_resource render - expect(d[0].foo).to eq("foo!") + expect(jsonapi_data[0].foo).to eq("foo!") end context "when application serializer defined" do @@ -117,7 +117,7 @@ def my_method it "can call methods on the namespaced ApplicationSerializer" do render - expect(d[0].foo).to eq("bar!") + expect(jsonapi_data[0].foo).to eq("bar!") end end @@ -131,7 +131,7 @@ def my_method it "can call methods on ApplicationSerializer" do render - expect(d[0].foo).to eq("bar!") + expect(jsonapi_data[0].foo).to eq("bar!") end end @@ -141,7 +141,7 @@ def my_method it "cannot call methods on ApplicationSerializer" do define_resource render - expect(d[0].foo).to eq("foo!") + expect(jsonapi_data[0].foo).to eq("foo!") end end end @@ -159,6 +159,18 @@ def my_method render expect(attributes["age"]).to eq("1") end + + context "when the resource disables typecast_reads" do + before do + resource.typecast_reads = false + end + + it "does not coerce" do + PORO::Employee.create(age: 1) + render + expect(attributes["age"]).to eq(1) + end + end end context "when integer" do @@ -797,6 +809,7 @@ def admin? resource.has_many :positions, resource: PORO::PositionResource, readable: :admin? + resource.relationship_placeholders = true Graphiti.setup! PORO::Employee.create(first_name: "John") end @@ -1165,15 +1178,15 @@ def positions Graphiti.config.context_for_endpoint = nil end - context "when not autolinked by default" do + context "when links are off by default" do before do - resource.autolink = false + resource.relationship_links = false end - it "does not generate links by default" do + it "does not render the relationship at all" do resource.has_many :positions render - expect(positions).to_not have_key("links") + expect(json["data"][0]["relationships"]).to_not have_key("positions") end it "does generate links when link: true passed" do @@ -1197,10 +1210,18 @@ def positions .to eq("/special/positions?blah=1") end - context "when links_on_demand" do - around do |e| - Graphiti.config.with_option(:links_on_demand, true) do - e.run + context "when the sideload resource renders links on demand" do + let(:position_resource) do + Class.new(PORO::PositionResource) do + self.relationship_links = :on_demand + end + end + + before do + resource.has_many :positions, resource: position_resource do + link do |employee| + "/special/positions?blah=#{employee.id}" + end end end @@ -1214,21 +1235,68 @@ def positions end end + context "when a relationship overrides the resource default" do + context "with link: :on_demand on an always-linked resource" do + before do + resource.relationship_links = true + resource.has_many :positions, link: :on_demand + end + + it "strips the relationship by default" do + render + expect(positions).to be_nil + end + + context "and links are requested" do + before do + params[:links] = true + end + + it "renders the link" do + render + expect(positions["links"]["related"]).to be_present + end + end + end + + context "with link: true on an on-demand resource" do + before do + resource.relationship_links = :on_demand + resource.has_many :positions, link: true + end + + it "renders the link without being asked" do + render + expect(positions["links"]["related"]).to be_present + end + end + end + describe "placeholder relationships" do before do - resource.has_many :positions, link: true + resource.has_many :positions + resource.relationship_placeholders = true + end + + context "when placeholders are off, as they are by default" do + before do + resource.relationship_placeholders = false + end + + it "is not present" do + render + expect(json["data"][0]["relationships"]).to eq({}) + end end context "when links on demand" do - around do |e| - Graphiti.config.with_option(:links_on_demand, true) do - e.run - end + before do + resource.relationship_links = :on_demand end context "and ?links param given" do before do - graphiti_context.params = {links: true} + params[:links] = true end it "is present" do @@ -1246,12 +1314,6 @@ def positions end context "and links not on demand" do - around do |e| - Graphiti.config.with_option(:links_on_demand, false) do - e.run - end - end - context "and a relationship is sideloaded" do before do params[:include] = "positions" @@ -1273,16 +1335,18 @@ def positions end context "when only linking if requested" do - around do |e| - resource.autolink = true - params[:include] = "positions" - resource.has_many :positions - - Graphiti.config.with_option(:links_on_demand, true) do - e.run + let(:position_resource) do + Class.new(PORO::PositionResource) do + self.relationship_links = :on_demand end end + before do + resource.relationship_links = :on_demand + params[:include] = "positions" + resource.has_many :positions, resource: position_resource + end + context "and not requested in url" do it "does not render links" do render @@ -1308,9 +1372,9 @@ def positions end end - context "when autolinked by default" do + context "when links are on by default" do before do - resource.autolink = true + resource.relationship_links = true end context "and a has_many relationship" do @@ -1350,9 +1414,9 @@ def positions resource.has_many :positions, link: false end - it "does not link" do + it "does not render the relationship at all" do render - expect(positions).to_not have_key("links") + expect(json["data"][0]["relationships"]).to_not have_key("positions") end end @@ -1440,13 +1504,13 @@ def credit_card context "that does not have an index endpoint" do before do Graphiti.config.context_for_endpoint = ->(path, action) { - action == :index ? nil : double + (action == :index) ? nil : double } end - context "when validating endpoints" do + context "when validating links" do before do - resource.validate_endpoints = true + resource.validate_links = true end it "raises error" do @@ -1456,9 +1520,9 @@ def credit_card end end - context "when not validating endpoints" do + context "when not validating links" do before do - resource.validate_endpoints = false + resource.validate_links = false end it "does not raise error" do @@ -1474,9 +1538,9 @@ def credit_card Graphiti.config.context_for_endpoint = nil end - context "when validating endpoints" do + context "when validating links" do before do - resource.validate_endpoints = true + resource.validate_links = true end it "raises error" do @@ -1486,9 +1550,9 @@ def credit_card end end - context "when not validating endpoints" do + context "when not validating links" do before do - resource.validate_endpoints = false + resource.validate_links = false end it "does not raise error" do @@ -1558,11 +1622,10 @@ def define_relationship mastercard_resource.endpoint = nil end - it "does not link" do + it "does not render the relationship at all" do define_relationship render - credit_card = json["data"][0]["relationships"]["credit_card"] - expect(credit_card).to_not have_key("links") + expect(json["data"][0]["relationships"]).to_not have_key("credit_card") end end @@ -1691,13 +1754,13 @@ def classification context "that does not have a show endpoint" do before do Graphiti.config.context_for_endpoint = ->(path, action) { - action == :show ? nil : double + (action == :show) ? nil : double } end - context "when validating endpoints" do + context "when validating links" do before do - resource.validate_endpoints = true + resource.validate_links = true end it "raises error" do @@ -1707,9 +1770,9 @@ def classification end end - context "when not validating endpoints" do + context "when not validating links" do before do - resource.validate_endpoints = false + resource.validate_links = false end it "does not raise error" do @@ -1806,6 +1869,7 @@ def classification "attrpositions" end resource.has_many :positions + resource.relationship_placeholders = true end context "when relationship is included" do @@ -1869,7 +1933,8 @@ def admin? false end has_many :things, resource: PORO::PositionResource, - foreign_key: :employee_id + foreign_key: :employee_id + self.relationship_placeholders = true end end @@ -1988,7 +2053,7 @@ def get_cursor(index) context "on the resource" do before do - resource.cursor_paginatable = true + resource.page_cursors = true end it "renders cursors" do @@ -2006,7 +2071,7 @@ def get_cursor(index) context "and implicit page size" do before do - resource.default_page_size = 1 + resource.page_default_size = 1 end it "is respected in the offset" do diff --git a/spec/sideload/polymorphic_belongs_to_spec.rb b/spec/sideload/polymorphic_belongs_to_spec.rb index 6c80c647..0fff8f15 100644 --- a/spec/sideload/polymorphic_belongs_to_spec.rb +++ b/spec/sideload/polymorphic_belongs_to_spec.rb @@ -61,7 +61,7 @@ def self.name it "resolves each group synchronously by default" do expect(instance).not_to receive(:future_resolve) - expect(child_sideload).to receive(:resolve).with(parents, query, graph_parent) + expect(child_sideload).to receive(:sync_resolve).with(parents, query, graph_parent) instance.resolve(parents, query, graph_parent) end diff --git a/spec/sideload_spec.rb b/spec/sideload_spec.rb index 6551fd9b..72537860 100644 --- a/spec/sideload_spec.rb +++ b/spec/sideload_spec.rb @@ -26,6 +26,44 @@ def self.name let(:name) { :foo } let(:instance) { Class.new(described_class).new(name, opts) } + describe "#link_mode" do + subject { instance.link_mode } + + it "inherits the parent resource default" do + parent_resource_class.relationship_links = :on_demand + expect(instance.link_mode).to eq(:on_demand) + end + + context "when the link option is given" do + before do + opts[:link] = :on_demand + parent_resource_class.relationship_links = true + end + + it { is_expected.to eq(:on_demand) } + end + + context "when the link option is not a valid mode" do + before do + opts[:link] = :always + end + + it "raises" do + expect { instance } + .to raise_error(Graphiti::Errors::InvalidLinkRendering, /foo link must be true, false, or :on_demand/) + end + end + + context "when a link block is defined and the default is false" do + before do + parent_resource_class.relationship_links = false + instance.class.link_proc = proc { "/foo" } + end + + it { is_expected.to eq(true) } + end + end + context "when passed both :remote and :resource options" do before do opts[:remote] = "asdf" diff --git a/spec/sideloading_spec.rb b/spec/sideloading_spec.rb index 8aaacb95..b0e6cd04 100644 --- a/spec/sideloading_spec.rb +++ b/spec/sideloading_spec.rb @@ -68,7 +68,7 @@ class PositionSideload < ::Graphiti::Sideload::HasMany it "works" do params[:include] = "positions" render - expect(included("positions").map(&:id)).to eq([1, 2]) + expect(jsonapi_included("positions").map(&:id)).to eq([1, 2]) end context "and deep querying" do @@ -79,7 +79,7 @@ class PositionSideload < ::Graphiti::Sideload::HasMany it "works" do render - expect(included("positions").map(&:id)).to eq([2, 1]) + expect(jsonapi_included("positions").map(&:id)).to eq([2, 1]) end end @@ -171,7 +171,7 @@ class PositionSideload < ::Graphiti::Sideload::HasMany it "works" do render - expect(included("positions").map(&:id)).to eq([1, 2]) + expect(jsonapi_included("positions").map(&:id)).to eq([1, 2]) end end @@ -196,7 +196,7 @@ class PositionSideload < ::Graphiti::Sideload::HasMany it "works" do render - expect(included("positions").map(&:id)).to eq([2, 1]) + expect(jsonapi_included("positions").map(&:id)).to eq([2, 1]) end end @@ -210,7 +210,7 @@ class PositionSideload < ::Graphiti::Sideload::HasMany it "works" do render - expect(included("positions").map(&:id)).to eq([2, 1]) + expect(jsonapi_included("positions").map(&:id)).to eq([2, 1]) end end @@ -231,7 +231,7 @@ class PositionSideload < ::Graphiti::Sideload::HasMany it "works" do render - expect(included("positions").map(&:id)).to eq([1, 2]) + expect(jsonapi_included("positions").map(&:id)).to eq([1, 2]) end context "when custom foreign key given" do @@ -254,7 +254,7 @@ class PositionSideload < ::Graphiti::Sideload::HasMany it "is used" do render - expect(included("positions").map(&:id)).to eq([1, 2]) + expect(jsonapi_included("positions").map(&:id)).to eq([1, 2]) end end end @@ -286,7 +286,7 @@ def self.name it "works" do render - expect(included("employees").map(&:id)).to eq([1]) + expect(jsonapi_included("employees").map(&:id)).to eq([1]) end end @@ -308,7 +308,7 @@ def self.name it "works" do render - expect(included("bios").map(&:id)).to eq([1]) + expect(jsonapi_included("bios").map(&:id)).to eq([1]) end end @@ -330,7 +330,7 @@ def self.name it "works" do render - expect(included("teams").map(&:id)).to eq([1, 2]) + expect(jsonapi_included("teams").map(&:id)).to eq([1, 2]) end end @@ -364,14 +364,10 @@ def assert_correct_response expect(credit_card(1)).to eq({ "type" => "mastercards", "id" => "1" }) - expect(included[0].jsonapi_type).to eq("visas") - expect(included[0].relationships).to eq({ - "visa_rewards" => {"meta" => {"included" => false}} - }) - expect(included[1].jsonapi_type).to eq("mastercards") - expect(included[1].relationships).to eq({ - "commercials" => {"meta" => {"included" => false}} - }) + expect(jsonapi_included[0].jsonapi_type).to eq("visas") + expect(jsonapi_included[0].relationships).to eq({}) + expect(jsonapi_included[1].jsonapi_type).to eq("mastercards") + expect(jsonapi_included[1].relationships).to eq({}) end context "when defaults" do @@ -421,9 +417,30 @@ def assert_correct_response end end + context "when multiple children error under concurrency" do + before do + allow(Graphiti.config).to receive(:concurrency).and_return(true) + resource.polymorphic_belongs_to :credit_card do + group_by(:credit_card_type, except: [:Paypal]) do + on(:Visa) + on(:Mastercard) + end + end + resource.sideloads[:credit_card].children.each_value do |child| + allow(child).to receive(:future_resolve) do + Concurrent::Promises.future { raise "#{child.group_name} failed" } + end + end + end + + it "raises the first child's error, not an aggregate" do + expect { render }.to raise_error(RuntimeError, "Visa failed") + end + end + context "when linking unknown type" do before do - Graphiti::Resource.autolink = true + Graphiti::Resource.relationship_links = true params.delete(:include) params[:links] = true resource.polymorphic_belongs_to :credit_card do @@ -435,14 +452,14 @@ def assert_correct_response end after do - Graphiti::Resource.autolink = false + Graphiti::Resource.relationship_links = false end it "does not blow up" do render - expect(d[0].link(:credit_card, :related)).to be_present - expect(d[1].link(:credit_card, :related)).to be_present - expect(d[2].link(:credit_card, :related)).to be_nil + expect(jsonapi_data[0].link(:credit_card, :related)).to be_present + expect(jsonapi_data[1].link(:credit_card, :related)).to be_present + expect(jsonapi_data[2].link(:credit_card, :related)).to be_nil end end @@ -459,7 +476,7 @@ def assert_correct_response it "does not add excluded relationships" do render - expect(included.map(&:jsonapi_type)).to contain_exactly("visas") + expect(jsonapi_included.map(&:jsonapi_type)).to contain_exactly("visas") end end @@ -477,7 +494,7 @@ def assert_correct_response it "only builds specified relationships" do render - expect(included.map(&:jsonapi_type)).to contain_exactly("visas", "mastercards") + expect(jsonapi_included.map(&:jsonapi_type)).to contain_exactly("visas", "mastercards") end end @@ -515,16 +532,12 @@ def assert_correct_response expect(payment_processor(2)).to eq({ "type" => "paypals", "id" => "1" }) - expect(included[0].jsonapi_type).to eq("visas") - expect(included[0].relationships).to eq({ - "visa_rewards" => {"meta" => {"included" => false}} - }) - expect(included[1].jsonapi_type).to eq("mastercards") - expect(included[1].relationships).to eq({ - "commercials" => {"meta" => {"included" => false}} - }) - expect(included[2].jsonapi_type).to eq("paypals") - expect(included[2].relationships).to be_nil + expect(jsonapi_included[0].jsonapi_type).to eq("visas") + expect(jsonapi_included[0].relationships).to eq({}) + expect(jsonapi_included[1].jsonapi_type).to eq("mastercards") + expect(jsonapi_included[1].relationships).to eq({}) + expect(jsonapi_included[2].jsonapi_type).to eq("paypals") + expect(jsonapi_included[2].relationships).to be_nil end it "works" do @@ -610,20 +623,33 @@ def type end end + describe "the included member" do + it "is present, and empty, when the include turns up nothing" do + params[:include] = "credit_cards" + render + expect(json["included"]).to eq([]) + end + + it "is absent when nothing was included" do + render + expect(json).to_not have_key("included") + end + end + context "when the associated resource has default pagination" do before do resource.class_eval do allow_sideload :positions, class: Sideloading::PositionSideload end resource.class_eval do - self.default_page_size = 1 + self.page_default_size = 1 end params[:include] = "positions" end it "is ignored for sideloads" do render - expect(included("positions").map(&:id)).to match_array([1, 2]) + expect(jsonapi_included("positions").map(&:id)).to match_array([1, 2]) end end @@ -671,8 +697,8 @@ def type it "works" do render - expect(included("positions").map(&:id)).to match_array([1, 2]) - expect(included("departments").map(&:id)).to match_array([1, 2]) + expect(jsonapi_included("positions").map(&:id)).to match_array([1, 2]) + expect(jsonapi_included("departments").map(&:id)).to match_array([1, 2]) end end @@ -707,7 +733,7 @@ def type it "works" do params[:page][:positions] = {size: 1} render - expect(included("positions").map(&:id)).to match_array([2]) + expect(jsonapi_included("positions").map(&:id)).to match_array([2]) end context "with offset" do @@ -717,7 +743,7 @@ def type it "works" do render - expect(included("positions").map(&:id)).to match_array([1]) + expect(jsonapi_included("positions").map(&:id)).to match_array([1]) end end @@ -729,7 +755,7 @@ def type it "works" do render - expect(included("positions").map(&:id)).to match_array([1]) + expect(jsonapi_included("positions").map(&:id)).to match_array([1]) end end end @@ -757,7 +783,7 @@ def self.name it "works" do render - expect(included("positions").map(&:id)).to match_array([1, 2]) + expect(jsonapi_included("positions").map(&:id)).to match_array([1, 2]) end context "but primary key is nil" do @@ -793,7 +819,7 @@ def self.name it "works" do render - sl = d[0].sideload(:positions) + sl = jsonapi_data[0].sideload(:positions) expect(sl.map(&:id)).to eq([position1.id, position2.id]) end end @@ -838,7 +864,7 @@ def self.name it "is respected" do render - expect(included("positions").map(&:id)).to match_array([2]) + expect(jsonapi_included("positions").map(&:id)).to match_array([2]) end end @@ -853,7 +879,7 @@ def self.name it "is respected" do render - expect(included("positions").map(&:id)).to match_array([2]) + expect(jsonapi_included("positions").map(&:id)).to match_array([2]) end end end @@ -883,7 +909,7 @@ def self.name it "works" do render - expect(included("departments").map(&:id)).to match_array([1, 2]) + expect(jsonapi_included("departments").map(&:id)).to match_array([1, 2]) end context "but the foreign key is nil" do @@ -895,8 +921,8 @@ def self.name it "returns nil without querying" do expect(department_resource).to_not receive(:all) render - expect(d[0].sideload("department")).to be_nil - expect(d[1].sideload("department")).to be_nil + expect(jsonapi_data[0].sideload("department")).to be_nil + expect(jsonapi_data[1].sideload("department")).to be_nil end context "but params customization" do @@ -921,7 +947,7 @@ def self.name it "works" do render - sl = d[0].sideload(:department) + sl = jsonapi_data[0].sideload(:department) expect(sl.id).to eq(department.id) end @@ -932,7 +958,7 @@ def self.name it "works" do render - sl = d[0].sideload(:department) + sl = jsonapi_data[0].sideload(:department) expect(sl.id).to eq(department.id) end end @@ -950,7 +976,7 @@ def self.name it "is respected" do render - expect(included("departments").map(&:id)).to match_array([2]) + expect(jsonapi_included("departments").map(&:id)).to match_array([2]) end end @@ -965,7 +991,7 @@ def self.name it "is respected" do render - expect(included("departments").map(&:id)).to match_array([2]) + expect(jsonapi_included("departments").map(&:id)).to match_array([2]) end end end @@ -993,7 +1019,7 @@ def self.name it "works" do render - expect(included("bios").map(&:id)).to match_array([1]) + expect(jsonapi_included("bios").map(&:id)).to match_array([1]) end context "and params customization" do @@ -1007,7 +1033,7 @@ def self.name it "is respected" do render - expect(included("bios").map(&:id)).to match_array([2]) + expect(jsonapi_included("bios").map(&:id)).to match_array([2]) end end @@ -1022,7 +1048,7 @@ def self.name it "is respected" do render - expect(included("bios").map(&:id)).to match_array([2]) + expect(jsonapi_included("bios").map(&:id)).to match_array([2]) end end end @@ -1089,9 +1115,18 @@ def department expect(pos1.length).to eq(1) end + it "does not assign the same children twice when a resource repeats in the include path" do + params[:include] = "positions.department.positions" + json = PORO::DepartmentResource.all(params).to_jsonapi + rendered = JSON.parse(json)["data"].map { |record| + record.dig("relationships", "positions", "data").map { |ref| ref["id"] } + } + expect(rendered).to all(satisfy { |ids| ids == ids.uniq }) + end + it "has all correct assocations" do render - sl = d[0].sideload(:current_position) + sl = jsonapi_data[0].sideload(:current_position) expect(sl.id).to eq(1) expect(sl.jsonapi_type).to eq("positions") expect(sl.sideload(:department)).to be_present @@ -1106,6 +1141,100 @@ def department }.to_not raise_error end + context "when a resource-provided association populates only one copy" do + around do |example| + example.run + ensure + PORO::Position.class_eval do + remove_method :custom_department + remove_method :custom_department= + end + end + + before do + PORO::Position.class_eval do + attr_accessor :custom_department + end + + position_resource.belongs_to :custom_department, resource: department_resource, foreign_key: :department_id do + assign_each do |position, _| + position.department + end + end + + params[:include] = "current_position.custom_department,positions" + end + + def squashed_position1 + matches = json["included"].select { |i| + i["type"] == "positions" && i["id"] == position1.id.to_s + } + expect(matches.length).to eq(1) + matches.first + end + + it "renders complete relationship data on the squashed node" do + render + + relationships = squashed_position1["relationships"] + expect(relationships).to be_present + expect(relationships["custom_department"]).to be_present + expect(relationships["custom_department"]["data"]).to be_present + end + + context "and concurrency is enabled" do + before do + allow(Graphiti.config).to receive(:concurrency).and_return(true) + end + + it "renders complete relationship data through the promise path" do + render + + relationships = squashed_position1["relationships"] + expect(relationships["custom_department"]["data"]).to be_present + end + end + end + + it "resolves one instance per row within a resource" do + proxy = resource.all(params) + employee = proxy.data[0] + twin = employee.positions.find { |position| position.id == employee.current_position.id } + + expect(employee.current_position).to equal(twin) + end + + it "keeps instances separate across resources serving the same model" do + variant = Class.new(position_resource) do + def self.name + "PORO::SpecialPositionResource" + end + end + resource.sideloads.delete(:current_position) + resource.has_one :current_position, resource: variant + + proxy = resource.all(params) + employee = proxy.data[0] + twin = employee.positions.find { |position| position.id == employee.current_position.id } + + expect(employee.current_position).to_not equal(twin) + end + + it "keeps instances separate when the sideload customizes its scope" do + resource.sideloads.delete(:current_position) + resource.has_one :current_position, resource: position_resource do + scope do |employee_ids| + {type: :positions, conditions: {employee_id: employee_ids}} + end + end + + proxy = resource.all(params) + employee = proxy.data[0] + twin = employee.positions.find { |position| position.id == employee.current_position.id } + + expect(employee.current_position).to_not equal(twin) + end + describe "across requests" do it "uses a different sideloaded resource" do ctx = double(current_user: :admin) diff --git a/spec/sidepost_attribute_callbacks_spec.rb b/spec/sidepost_attribute_callbacks_spec.rb new file mode 100644 index 00000000..f385aeff --- /dev/null +++ b/spec/sidepost_attribute_callbacks_spec.rb @@ -0,0 +1,86 @@ +require "spec_helper" + +# Inspecting the model before saving keeps the sideposted foreign key out of the hash the +# attributes callbacks receive. apply_derived_attributes still writes it onto the model. +RSpec.describe "attributes callbacks with a sideposted parent" do + let(:fired) { [] } + + let!(:classification_resource) do + Class.new(PORO::ApplicationResource) do + def self.name + "ScClassificationResource" + end + self.model = PORO::Classification + self.type = :classifications + attribute :description, :string + end + end + + let!(:employee_resource) do + classifications = classification_resource + recorder = fired + Class.new(PORO::ApplicationResource) do + def self.name + "ScEmployeeResource" + end + self.model = PORO::Employee + self.type = :employees + attribute :first_name, :string + attribute :classification_id, :integer, only: [:writable] + belongs_to :classification, resource: classifications + before_attributes { |attributes| recorder << attributes.keys } + end + end + + def payload(id = nil) + data = { + type: "employees", + attributes: {first_name: "Jane"}, + relationships: { + classification: {data: {type: "classifications", "temp-id": "abc", method: "create"}} + } + } + data[:id] = id.to_s if id + { + data: data, + included: [{type: "classifications", "temp-id": "abc", attributes: {description: "x"}}] + } + end + + before { PORO::DB.clear } + + context "creating" do + it "fires once with the derived foreign key included" do + employee_resource.build(payload).save + + expect(fired).to eq([[:first_name, :classification_id]]) + end + + it "fires without the foreign key when the model is inspected first" do + proxy = employee_resource.build(payload) + proxy.data + proxy.save + + expect(fired).to eq([[:first_name]]) + end + end + + context "updating" do + let!(:employee) { PORO::Employee.create(first_name: "old") } + + it "fires once with the derived foreign key included" do + employee_resource.find(payload(employee.id)).update_attributes + + expect(fired).to eq([[:first_name, :classification_id]]) + end + + it "fires without the foreign key when the model is inspected first" do + proxy = employee_resource.find(payload(employee.id)) + proxy.assign_attributes(payload(employee.id)) + proxy.data + proxy.update + + expect(fired).to eq([[:first_name]]) + end + end +end diff --git a/spec/simple_errors_spec.rb b/spec/simple_errors_spec.rb new file mode 100644 index 00000000..94388021 --- /dev/null +++ b/spec/simple_errors_spec.rb @@ -0,0 +1,72 @@ +RSpec.describe Graphiti::Util::SimpleErrors do + subject(:errors) { described_class.new(Object.new) } + + def with_translations(translations) + original = I18n.backend + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations(:en, graphiti: {errors: translations}) + yield + ensure + I18n.backend = original + end + + describe "#add" do + it "humanizes the code when given no message" do + errors.add(:name, :missing) + + expect(errors[:name]).to eq(["is missing"]) + end + + it "keeps the given message" do + errors.add(:name, :invalid, message: "must be an object") + + expect(errors[:name]).to eq(["must be an object"]) + end + + it "prefers a translation for the code" do + with_translations(messages: {invalid: "ustmay ebay anway objectway"}) do + errors.add(:name, :invalid, message: "must be an object") + end + + expect(errors[:name]).to eq(["ustmay ebay anway objectway"]) + end + + it "interpolates into the message" do + errors.add(:age, :type_error, message: "should be type %{type}", type: "integer") + + expect(errors[:age]).to eq(["should be type integer"]) + end + + it "gives a translation the attribute to name" do + with_translations(messages: {unknown_attribute: "%{attribute} isway unknownway"}) do + errors.add(:"data.attributes.foo", :unknown_attribute) + end + + expect(errors[:"data.attributes.foo"]).to eq(["data.attributes.foo isway unknownway"]) + end + + it "interpolates into a translation" do + with_translations(messages: {type_error: "ouldshay ebay ypetay %{type}"}) do + errors.add(:age, :type_error, message: "should be type %{type}", type: "integer") + end + + expect(errors[:age]).to eq(["ouldshay ebay ypetay integer"]) + end + end + + describe "#full_message" do + it "reads attribute then message" do + expect(errors.full_message(:name, "is missing")).to eq("name is missing") + end + + it "takes its word order from a translation" do + with_translations(format: "%{message}: %{attribute}") do + expect(errors.full_message(:name, "is missing")).to eq("is missing: name") + end + end + + it "leaves a base error alone" do + expect(errors.full_message(:base, "is missing")).to eq("is missing") + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 282c8c3e..2518acbb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,10 +5,10 @@ require "logger" require "active_model" -require "graphiti_spec_helpers/rspec" +require "graphiti/spec_helpers/rspec" require "graphiti" # Avoiding loading classes before we're ready -Graphiti::Resource.autolink = false +Graphiti::Resource.relationship_links = false require "fixtures/poro" Graphiti.setup! @@ -17,8 +17,7 @@ require "base64" RSpec.configure do |config| - config.include GraphitiSpecHelpers::RSpec - config.include GraphitiSpecHelpers::Sugar + config.include Graphiti::SpecHelpers::RSpec config.after do PORO::DB.clear @@ -42,9 +41,11 @@ # earlier examples defined - which makes results depend on spec order. config.around do |example| registered = Graphiti.resources.dup + setup_was = Graphiti.setup? example.run ensure Graphiti.resources.replace(registered) + Graphiti.instance_variable_set(:@setup, setup_was) end config.filter_run_when_matching :focus @@ -61,12 +62,6 @@ end end - # Avoid checking, because Rails is defined but we dont have autoloading - Graphiti::Sideload.class_eval do - def check! - end - end - require "database_cleaner" require "kaminari" require "active_record" @@ -77,13 +72,3 @@ def check! database: ":memory:" Dir[File.dirname(__FILE__) + "/fixtures/**/*.rb"].sort.each { |f| require f } end - -if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2.0") - unless defined?(::ActionDispatch::Journey) - require "uri" - # NOTE: `decode_www_form_component` isn't an ideal default for production, - # because it varies slightly compared to typical uri parameterization, - # but it will allow tests to pass in non-rails contexts. - Graphiti.config.uri_decoder = URI.method(:decode_www_form_component) - end -end diff --git a/spec/spec_helpers_matchers_spec.rb b/spec/spec_helpers_matchers_spec.rb new file mode 100644 index 00000000..3f2b38e2 --- /dev/null +++ b/spec/spec_helpers_matchers_spec.rb @@ -0,0 +1,146 @@ +require "spec_helper" + +RSpec.describe Graphiti::SpecHelpers::Matchers do + include Graphiti::SpecHelpers::Matchers + + let(:author_resource) do + Class.new(PORO::ApplicationResource) do + def self.name + "AuthorResource" + end + + # belongs_to infers its foreign key from the related resource's model. + self.model = PORO::Employee + + attribute :name, :string + end + end + + let(:post_resource) do + author = author_resource + Class.new(PORO::ApplicationResource) do + def self.name + "PostResource" + end + + # Asserting on foreign_key makes the sideload infer one from the model, + # which an anonymous resource has no way to find. + self.model = PORO::Employee + + attribute :title, :string, sortable: false + filter :body, :string, blanks: :null + + belongs_to :author, resource: author + has_many :comments, resource: author + has_one :summary, resource: author + end + end + + subject(:resource) { post_resource.new } + + describe "#belong_to_resource" do + it { is_expected.to belong_to_resource(:author) } + + it "fails on an unknown relationship" do + expect(resource).not_to belong_to_resource(:nope) + end + + it "fails when the relationship is a different type" do + expect(resource).not_to belong_to_resource(:comments) + end + + it "matches on options" do + expect(resource).to belong_to_resource(:author) + .with_options(resource: author_resource) + end + + it "fails when an option does not match" do + expect(resource).not_to belong_to_resource(:author) + .with_options(foreign_key: :wrong_id) + end + + it "matches on options backed by predicate methods" do + expect(resource).to belong_to_resource(:author) + .with_options(readable: true, writable: true, single: false) + end + + it "fails when a predicate-backed option does not match" do + expect(resource).not_to belong_to_resource(:author) + .with_options(readable: false) + end + end + + describe "#have_many_resources" do + it { is_expected.to have_many_resources(:comments) } + + it "fails when the relationship is a different type" do + expect(resource).not_to have_many_resources(:author) + end + end + + describe "#have_one_resource" do + it { is_expected.to have_one_resource(:summary) } + + it "fails when the relationship is a different type" do + expect(resource).not_to have_one_resource(:comments) + end + end + + describe "#expose_attribute" do + it { is_expected.to expose_attribute(:title, :string) } + + it "fails on an unknown attribute" do + expect(resource).not_to expose_attribute(:nope, :string) + end + + it "fails on the wrong type" do + expect(resource).not_to expose_attribute(:title, :integer) + end + + it "matches on options" do + expect(resource).to expose_attribute(:title, :string) + .with_options(sortable: false) + end + + it "fails when an option does not match" do + expect(resource).not_to expose_attribute(:title, :string) + .with_options(sortable: true) + end + end + + describe "#filter_attribute" do + it { is_expected.to filter_attribute(:body, :string) } + + it "fails on an unknown filter" do + expect(resource).not_to filter_attribute(:nope, :string) + end + + it "matches on options" do + expect(resource).to filter_attribute(:body, :string) + .with_options(blanks: :null) + end + + it "fails when an option does not match" do + expect(resource).not_to filter_attribute(:body, :string) + .with_options(blanks: :literal) + end + end + + describe "failure messages" do + it "names the resource and the expectation" do + matcher = belong_to_resource(:nope) + matcher.matches?(resource) + + expect(matcher.failure_message) + .to include(post_resource.to_s, "belong to", "nope") + end + + it "reports which option did not match" do + matcher = expose_attribute(:title, :string).with_options(sortable: true) + matcher.matches?(resource) + + expect(matcher.failure_message) + .to include("expected that sortable would be true, was false") + end + end +end diff --git a/spec/spec_helpers_spec.rb b/spec/spec_helpers_spec.rb new file mode 100644 index 00000000..af3f5279 --- /dev/null +++ b/spec/spec_helpers_spec.rb @@ -0,0 +1,249 @@ +require "spec_helper" + +describe Graphiti::SpecHelpers do + include Graphiti::SpecHelpers + + attr_accessor :response + + let(:instance) { klass.new } + + let(:json) do + { + data: [ + { + type: "employees", + id: "100", + attributes: {first_name: "John"}, + relationships: { + positions: { + data: [ + { + type: "positions", + id: "200" + } + ], + links: { + related: "http://example.com/positions?filter[employee_id]=100" + } + } + } + } + ], + included: [ + { + type: "positions", + id: "200", + attributes: {title: "Manager"}, + relationships: { + department: { + data: { + type: "departments", + id: "300" + } + } + } + }, + { + type: "departments", + id: "300", + attributes: {name: "Engineering"} + } + ] + }.with_indifferent_access + end + + describe "#link" do + it "returns the relevant link" do + link = jsonapi_data[0].link(:positions, :related) + expect(link).to eq("http://example.com/positions?filter[employee_id]=100") + end + + context "when no links" do + before do + json[:data][0][:relationships][:positions].delete(:links) + end + + it "raises helpful error" do + expect { + jsonapi_data[0].link(:positions, :related) + }.to raise_error(Graphiti::SpecHelpers::Errors::LinksNotFound, "Relationship with name 'positions' has no links!") + end + end + end + + describe "#flatten" do + it "works" do + expect(jsonapi_data.flatten.map(&:id)).to eq([100]) + end + end + + describe "#sideloads/sideload" do + context "when relation is an array" do + it "returns relevant node" do + sl = jsonapi_data[0].sideloads(:positions) + expect(sl.map(&:attributes)).to eq([{ + "id" => "200", + "jsonapi_type" => "positions", + "title" => "Manager" + }]) + end + end + + context "when relation is a hash" do + it "returns relevant node" do + sl = jsonapi_included[0].sideload(:department) + expect(sl.attributes).to eq({ + "id" => "300", + "jsonapi_type" => "departments", + "name" => "Engineering" + }) + end + end + + context "when not found" do + it "raises helpful error" do + expect { + jsonapi_data[0].sideloads(:foo) + }.to raise_error(Graphiti::SpecHelpers::Errors::SideloadNotFound, "Relationship with name 'foo' not found!") + end + end + + context "when nil" do + before do + json[:included][0][:relationships][:department][:data] = nil + end + + it "returns nil" do + expect(jsonapi_included[0].sideload(:department)).to be_nil + end + end + end + + describe "#jsonapi_data" do + context "when data is hash" do + before do + json[:data] = json[:data][0] + end + + it "returns a node" do + expect(jsonapi_data.is_a?(Graphiti::SpecHelpers::Node)).to eq(true) + expect(jsonapi_data.id).to eq(100) + expect(jsonapi_data.jsonapi_type).to eq("employees") + expect(jsonapi_data.first_name).to eq("John") + end + + context "when trying to access missing attribute" do + it "raises helpful error" do + expect { + jsonapi_data.foo + }.to raise_error(Graphiti::SpecHelpers::Errors::NoAttribute, "No attribute 'foo' in JSON response node!") + end + end + end + + context "when data is an array" do + it "returns an array of nodes" do + expect(jsonapi_data.is_a?(Array)).to eq(true) + expect(jsonapi_data.length).to eq(1) + expect(jsonapi_data[0].is_a?(Graphiti::SpecHelpers::Node)).to eq(true) + expect(jsonapi_data[0].id).to eq(100) + expect(jsonapi_data[0].jsonapi_type).to eq("employees") + expect(jsonapi_data[0].first_name).to eq("John") + end + end + + context "when no data" do + before do + json.delete("data") + end + + it "raises helpful error" do + expect { + jsonapi_data + }.to raise_error(Graphiti::SpecHelpers::Errors::NoData) + end + end + end + + describe "#jsonapi_included" do + it "returns an array of nodes from included section of payload" do + i = jsonapi_included + expect(i.is_a?(Array)).to eq(true) + expect(i.length).to eq(2) + expect(i[0].id).to eq(200) + expect(i[0].jsonapi_type).to eq("positions") + expect(i[0].title).to eq("Manager") + expect(i[1].id).to eq(300) + expect(i[1].jsonapi_type).to eq("departments") + expect(i[1].name).to eq("Engineering") + end + end + + describe "#jsonapi_errors" do + let(:json) do + { + errors: [ + { + code: "unprocessable_entity", + status: "422", + title: "Validation Error", + detail: "Name can't be blank", + source: {pointer: "/data/attributes/name"}, + meta: { + attribute: :name, + message: "can't be blank", + code: :blank + } + } + ] + }.with_indifferent_access + end + + it "returns a proxy that acts as array" do + errors = jsonapi_errors + expect(errors.length).to eq(1) + expect(errors[0].attribute).to eq(:name) + expect(errors[0].status).to eq("422") + expect(errors[0].title).to eq("Validation Error") + expect(errors[0].detail).to eq("Name can't be blank") + expect(errors[0].code).to eq(:blank) + expect(errors[0].message).to eq("can't be blank") + end + + it "finds errors via attribute name" do + errors = jsonapi_errors + expect(errors.name.message).to eq("can't be blank") + end + + it "can render simple hash" do + errors = jsonapi_errors + expect(errors.to_h).to eq({ + name: "can't be blank" + }) + end + end + + describe "#jsonapi_meta" do + context "when data is present" do + before do + json[:meta] = {foo: "bar"} + end + + it "returns the hash" do + expect(jsonapi_meta).to eq({"foo" => "bar"}) + end + end + + context "when no meta" do + before do + json.delete("meta") + end + + it "raises helpful error" do + expect { + jsonapi_meta + }.to raise_error(Graphiti::SpecHelpers::Errors::NoMeta) + end + end + end +end diff --git a/spec/stress/concurrency_stress_spec.rb b/spec/stress/concurrency_stress_spec.rb index b886b60d..43eba85b 100644 --- a/spec/stress/concurrency_stress_spec.rb +++ b/spec/stress/concurrency_stress_spec.rb @@ -17,6 +17,20 @@ def employee_resource(&definition) end end + # The request thread resolves the root, so only pool threads are of interest. + def threads_used(*resource_classes) + request_thread = Thread.current.object_id + threads = Concurrent::Array.new + resource_classes.each do |resource_class| + allow_any_instance_of(resource_class).to receive(:resolve).and_wrap_original do |original, *args| + threads << Thread.current.object_id unless Thread.current.object_id == request_thread + original.call(*args) + end + end + yield + threads + end + def resolve(resource_class, params) resource_class.all({page: {size: 100}}.merge(params)).to_a end @@ -105,6 +119,70 @@ def resolve(resource_class, params) end end + describe "two sideloads at the top level" do + before { seed_employees(count: 20, positions_per_employee: 3, departments: 4) } + + let(:two_branches) do + employee_resource do + has_many :positions, resource: PORO::PositionResource + has_many :credit_cards, resource: PORO::CreditCardResource + end + end + + it "resolves them on separate threads" do + threads = threads_used(PORO::PositionResource, PORO::CreditCardResource) do + resolve(two_branches, include: "positions,credit_cards") + end + + expect(threads.uniq.length).to be > 1 + end + end + + describe "two sideloads under the same parent" do + before { seed_employees(count: 20, positions_per_employee: 3, departments: 4) } + + let(:nested_siblings) do + Class.new(PORO::PositionResource) do + def self.name + "PORO::PositionResource" + end + + belongs_to :employee, resource: PORO::EmployeeResource, foreign_key: :employee_id + end + end + + let(:parent) do + sibling_resource = nested_siblings + employee_resource { has_many :positions, resource: sibling_resource } + end + + # Siblings a level down are independent queries, so they resolve in parallel. + it "resolves them on separate threads" do + threads = threads_used(PORO::DepartmentResource, PORO::EmployeeResource) do + resolve(parent, include: "positions.department,positions.employee") + end + + expect(threads.uniq.length).to be > 1 + end + + it "keeps both branches populated under concurrent load" do + resource_class = parent + + results = run_concurrent_requests(count: pool_size * 2, timeout: 30) do + resolve(resource_class, include: "positions.department,positions.employee") + end + + results.each do |employees| + employees.each do |employee| + employee.positions.each do |position| + expect(position.department).not_to be_nil + expect(position.employee).not_to be_nil + end + end + end + end + end + describe "deep include trees" do before { seed_employees(count: 30, positions_per_employee: 4, departments: 5) } diff --git a/spec/support/file_backed_database.rb b/spec/support/file_backed_database.rb new file mode 100644 index 00000000..651e4faa --- /dev/null +++ b/spec/support/file_backed_database.rb @@ -0,0 +1,23 @@ +# Threaded specs need a real file, because sqlite's :memory: gives each connection its own empty database. +module FileBackedDatabase + module_function + + def connect(namespace, path, pool: 5) + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.rm_f(path) + base = Class.new(ActiveRecord::Base) { self.abstract_class = true } + namespace.const_set(:Base, base) + base.establish_connection( + adapter: "sqlite3", database: path, pool: pool, timeout: 5000 + ) + base + end + + def disconnect(namespace, path, consts) + namespace::Base.remove_connection + FileUtils.rm_f(path) + (consts + [:Base]).each do |name| + namespace.send(:remove_const, name) if namespace.const_defined?(name, false) + end + end +end diff --git a/spec/support/rails/universal_controller_spec_helper.rb b/spec/support/rails/universal_controller_spec_helper.rb index f1add894..bc56cbff 100644 --- a/spec/support/rails/universal_controller_spec_helper.rb +++ b/spec/support/rails/universal_controller_spec_helper.rb @@ -1,10 +1,6 @@ module UniversalControllerSpecHelper def universal_process(method, action, params) - if Rails::VERSION::MAJOR == 4 - send(method, action, params) - else - send(method, action, params: params) - end + send(method, action, params: params) end def do_create(params) diff --git a/spec/writable_guard_action_spec.rb b/spec/writable_guard_action_spec.rb new file mode 100644 index 00000000..386c2506 --- /dev/null +++ b/spec/writable_guard_action_spec.rb @@ -0,0 +1,41 @@ +require "spec_helper" + +RSpec.describe "writable guard action" do + let(:seen) { [] } + + let!(:resource) do + actions = seen + Class.new(PORO::ApplicationResource) do + def self.name + "GuardEmployeeResource" + end + self.model = PORO::Employee + self.type = :employees + attribute :first_name, :string, writable: proc { + actions << Graphiti.context[:action] + true + } + define_singleton_method(:actions) { actions } + end + end + + let(:payload) do + {data: {type: "employees", attributes: {first_name: "Jane"}}} + end + + before { PORO::DB.clear } + + it "sees :create when the model was not inspected" do + Graphiti.with_context({}, :all) { resource.build(payload).save } + expect(seen).to include(:create) + end + + it "sees :create when the model was inspected first" do + Graphiti.with_context({}, :all) do + proxy = resource.build(payload) + proxy.data + proxy.save + end + expect(seen).to include(:create) + end +end diff --git a/templates/rails/all.rb b/templates/rails/all.rb new file mode 100644 index 00000000..0250126b --- /dev/null +++ b/templates/rails/all.rb @@ -0,0 +1,114 @@ +Thor::Base.shell = Thor::Shell::Color +require "yaml" + +def update_config!(attrs) + config = File.exist?(".graphiticfg.yml") ? YAML.load_file(".graphiticfg.yml") : {} + config.merge!(attrs) + File.write(".graphiticfg.yml", config.to_yaml) +end + +def prompt(header: nil, description: nil, default: nil) + say(set_color("\n#{header}", :magenta, :bold)) if header + say("\n#{description}") if description + answer = ask(set_color("\n(default: #{default}):", :magenta, :bold)) + answer = default if answer.blank? && default != "nil" + say(set_color("\nGot it!\n", :white, :bold)) + answer +end + +def api_namespace + @api_namespace ||= begin + ns = prompt \ + header: "What is your API namespace?", + description: "This will be used as a route prefix, e.g. if you want the route '/books_api/v1/authors' your namespace would be '/books_api/v1'", + default: "/api/v1" + update_config!("namespace" => ns) + ns + end +end + +welcome = <<-STR +\n +Welcome to the Graphiti generator! +======================================= + +This will take care of some boilerplate for you, like adding gem dependencies and rspec helpers. + +If you're worried there might be too much magic here, feel free to run 'git diff' at the end to see what happened. You can also learn more at our documentation website, https://graphiti.dev +STR + +say(set_color(welcome.rstrip, :cyan, :bold)) +api_namespace + +gem "graphiti", "~> 2.0.0.beta" +gem "vandal_ui" +gem "kaminari" + +gem_group :development, :test do + gem "rspec-rails" + gem "factory_bot_rails" + gem "faker" # keep here for seeds.rb +end + +gem_group :test do + gem "database_cleaner-active_record", "~> 2.2" +end + +after_bundle do + rails_command "generate rspec:install" + run "rm -rf test" + + insert_into_file "spec/rails_helper.rb", after: "RSpec.configure do |config|\n" do + <<-STR + config.include FactoryBot::Syntax::Methods + config.include Graphiti::Rails::TestHelpers + + config.before :each do + handle_request_exceptions(false) + end + + config.before(:suite) do + DatabaseCleaner.strategy = :transaction + DatabaseCleaner.clean_with(:truncation) + end + + config.around(:each) do |example| + DatabaseCleaner.cleaning do + example.run + end + ensure + DatabaseCleaner.clean + end + + STR + end + + run "mkdir -p spec/factories" + + rails_command "generate graphiti:install" + rake "vandal:install" + + git :init + git add: "." + + next_steps = <<-STR + +You're all set! Next steps: + + Define a model, then generate its Resource, controller, route and specs in one go: + + $ bin/rails generate model post title:string upvotes:integer + $ bin/rails db:migrate + $ bin/rails generate graphiti:resource Post title:string upvotes:integer + + Boot the app and try your API: + + $ bin/rails server + $ curl localhost:3000#{@api_namespace}/posts + + Or explore it visually with Vandal at localhost:3000#{@api_namespace}/vandal + + Full documentation: https://graphiti.dev +STR + say(set_color(next_steps, :green)) +end diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 00000000..b2d6de30 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,20 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/website/README.md b/website/README.md new file mode 100644 index 00000000..bce6eb2f --- /dev/null +++ b/website/README.md @@ -0,0 +1,43 @@ +# Website + +This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. + +## Installation + +```bash +npm install +``` + +**Note**: feel free to use the package manager of your choice. + +## Local Development + +```bash +npm run start +``` + +This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. + +## Build + +```bash +npm run build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + +## Deployment + +Using SSH: + +```bash +USE_SSH=true npm run deploy +``` + +Not using SSH: + +```bash +GIT_USER= npm run deploy +``` + +If you are using GitHub Pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js new file mode 100644 index 00000000..1b8dd336 --- /dev/null +++ b/website/docusaurus.config.js @@ -0,0 +1,197 @@ +// @ts-check +const {themes} = require('prism-react-renderer'); + +const codeBlockTheme = (theme, backgroundColor) => ({ + ...theme, + plain: {...theme.plain, backgroundColor}, +}); + +// Docusaurus prepends baseUrl to navbar and Link hrefs, but not to raw HTML +// like announcementBar content, so that markup has to build its own absolute +// paths. Always has a trailing slash. +const baseUrl = process.env.DOCS_BASE_URL || '/'; + +// The theme's strikethrough and underline on diff lines are inline styles, so +// CSS cannot undo them. Colour and the +/- signs already carry it. +const withoutDiffDecoration = (theme) => ({ + ...theme, + styles: theme.styles.map(({types, style}) => + types.some((type) => type === 'inserted' || type === 'deleted') + ? {types, style: {...style, textDecorationLine: undefined}} + : {types, style} + ), +}); + +/** @type {import('@docusaurus/types').Config} */ +const config = { + title: 'Graphiti', + tagline: 'Stylish Graph APIs', + favicon: 'img/favicon.ico', + + // Apex, matching the CNAME. This feeds canonical tags and sitemap.xml, so it + // has to agree with the host actually serving the site or the two split + // search ranking between them. + url: 'https://graphiti.dev', + // graphiti.dev resolves to this repo, so the site serves from the root. + // DOCS_BASE_URL still overrides it for a build served from the project-pages + // path, which is what /graphiti/ was during the beta. + baseUrl, + organizationName: 'graphiti-api', + projectName: 'graphiti', + + onBrokenLinks: 'warn', + markdown: {hooks: {onBrokenMarkdownLinks: 'warn'}}, + + presets: [ + [ + 'classic', + /** @type {import('@docusaurus/preset-classic').Options} */ + ({ + docs: { + // markdown lives at the repo root, next to the code it documents + path: '../docs', + routeBasePath: '/', + sidebarPath: require.resolve('./sidebars.js'), + editUrl: 'https://github.com/graphiti-api/graphiti/tree/main/docs/', + // Unversioned docs serve at the root, and would otherwise be + // labelled "Next" in the dropdown. Once `npm run docusaurus + // docs:version 2.0` cuts a version, that becomes the root and the + // working copy moves to /next. + versions: {current: {label: '2.0 beta'}}, + }, + blog: false, + theme: {customCss: require.resolve('./src/css/custom.css')}, + }), + ], + ], + + plugins: [ + [ + '@docusaurus/plugin-client-redirects', + { + // The 1.x Jekyll site served /guides/* and /cookbooks/*, and early 2.0 + // drafts served /quickstart and /cheatsheet. Keep those URLs alive. + // Nothing under /1.13/ is touched: that is the frozen 1.x site. + redirects: [ + {from: ['/quickstart'], to: '/getting-started/first-api'}, + {from: ['/cheatsheet', '/guides', '/guides/index'], to: '/'}, + {from: ['/guides/overview'], to: '/concepts/overview'}, + {from: ['/guides/why'], to: '/reference/why'}, + {from: ['/guides/vandal'], to: '/reference/vandal'}, + {from: ['/guides/upgrading', '/guides/upgrading-2-0', '/reference/upgrading-2-0', '/upgrading/controllers', '/upgrading/testing', '/upgrading/persistence-hooks', '/upgrading/exception-handling', '/upgrading/belongs-to-linkage', '/upgrading/without-rails'], to: '/upgrading'}, + {from: ['/guides/getting-started/installation'], to: '/getting-started/installation'}, + + {from: ['/guides/concepts/resources'], to: '/concepts/resources'}, + {from: ['/guides/concepts/endpoints'], to: '/concepts/endpoints'}, + {from: ['/guides/concepts/links'], to: '/concepts/links'}, + {from: ['/guides/concepts/backends-and-models'], to: '/concepts/backends-and-models'}, + {from: ['/guides/concepts/testing'], to: '/topics/testing'}, + {from: ['/guides/concepts/debugging'], to: '/topics/debugging'}, + {from: ['/guides/concepts/error-handling'], to: '/topics/error-handling'}, + {from: ['/guides/concepts/remote-resources', '/cookbooks/remote-resources'], to: '/topics/remote-resources'}, + + // Cookbook stubs that never had content now point at the real pages + {from: ['/cookbooks/authorization'], to: '/topics/authorization'}, + {from: ['/cookbooks/caching'], to: '/topics/caching'}, + {from: ['/cookbooks/etags'], to: '/topics/etags'}, + {from: ['/cookbooks/json_attributes'], to: '/topics/json-attributes'}, + {from: ['/cookbooks/openstruct-models'], to: '/topics/openstruct-models'}, + {from: ['/cookbooks/without-activerecord'], to: '/topics/without-activerecord'}, + {from: ['/cookbooks/customizing-sideloads'], to: '/topics/customizing-sideloads'}, + {from: ['/cookbooks/hopping-relationships'], to: '/topics/hopping-relationships'}, + + {from: ['/js/introduction'], to: '/js/'}, + { + from: [ + '/js/reads/index', + '/js/reads/filtering', + '/js/reads/sorting', + '/js/reads/pagination', + '/js/reads/fieldsets', + '/js/reads/includes', + '/js/reads/nested-queries', + '/js/reads/statistics', + ], + to: '/js/reads', + }, + { + from: [ + '/js/writes/index', + '/js/writes/validations', + '/js/writes/dirty-tracking', + '/js/writes/nested', + '/js/writes/deferred', + ], + to: '/js/writes', + }, + ], + + // The 2.0 docs were staged under /graphiti/ for the whole beta, so + // those URLs are in the wild. createRedirects sees every generated + // route, which the static /1.13/ tree is not: that one is by hand. + createRedirects(existingPath) { + return [`/graphiti${existingPath}`]; + }, + }, + ], + ], + + themeConfig: { + announcementBar: { + // Changing the id un-dismisses the bar for everyone who has closed it. + id: 'graphiti-2-0-upgrade', + content: `Graphiti 2.0 is out. Coming from 1.x? Start with the upgrade guide.`, + isCloseable: true, + }, + colorMode: { + // Dark for a first-time visitor. Flip respectPrefersColorScheme to true + // to follow the OS setting instead, in which case defaultMode only + // applies when the visitor has no preference either way. Once someone + // uses the navbar toggle their choice is remembered and wins over both. + defaultMode: 'dark', + respectPrefersColorScheme: false, + }, + navbar: { + title: 'Graphiti', + logo: {alt: 'Graphiti', src: 'img/logo.png'}, + items: [ + { + type: 'docsVersionDropdown', + position: 'right', + // 1.x stays as the original Jekyll site, frozen under /1.13. + // + // The pathname:// prefix is required. It is Docusaurus' escape hatch + // for a path that is served statically but is not an app route: + // without it the link renders as a react-router push, which finds no + // match and shows the 404 page even though the file is served fine. + // The prefix is stripped before baseUrl is applied, and it also makes + // the link count as external, hence target '_self' to keep it from + // opening in a new tab. + dropdownItemsAfter: [ + {href: 'pathname:///1.13/', label: '1.x', target: '_self'}, + ], + }, + {href: 'https://github.com/graphiti-api/graphiti', label: 'GitHub', position: 'right'}, + {href: 'https://discord.gg/wgqkMBsSRV', label: 'Discord', position: 'right'}, + ], + }, + footer: { + style: 'dark', + copyright: [ + 'Originally created by Lee Richmond,', + 'Wade Tandy, and', + 'Peter Wagenet.', + 'Maintained by Jeff Keen', + 'with many contributors.', + '
Graphiti is released under the MIT license.', + ].join(' '), + }, + prism: { + theme: withoutDiffDecoration(themes.oneLight), + darkTheme: withoutDiffDecoration(codeBlockTheme(themes.oneDark, '#21252b')), + additionalLanguages: ['ruby', 'bash', 'json', 'http', 'diff'], + }, + }, +}; + +module.exports = config; diff --git a/website/package-lock.json b/website/package-lock.json new file mode 100644 index 00000000..8a1fba9a --- /dev/null +++ b/website/package-lock.json @@ -0,0 +1,19474 @@ +{ + "name": "website", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "website", + "version": "0.0.0", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/faster": "3.10.2", + "@docusaurus/plugin-client-redirects": "^3.10.2", + "@docusaurus/preset-classic": "3.10.2", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "prism-react-renderer": "^2.3.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/types": "3.10.2" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@11ty/gray-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@11ty/gray-matter/-/gray-matter-1.0.0.tgz", + "integrity": "sha512-7mJJl+wf1AByoT0PknQiQfOPnVNT4fevGrUBVWO4HXsnYn1aQPyRyrELYrNUFleUBM++KzMKN6QaxHPk0t/6/g==", + "license": "MIT", + "dependencies": { + "js-yaml": "^4.1.0", + "kind-of": "^6.0.3", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=11" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.22.0.tgz", + "integrity": "sha512-BFR6zNowNKcY7Ou7TaJc9QWexES4YKPbmf/OTFofpdsdhz4x6q0lbxp3duO0EHnyrN7rE4ba/TSXuY+BDGu4+g==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.9.tgz", + "integrity": "sha512-4U2JKLMWlDu0CotYyUkWakDxr8AIav3QtIUXXRpfavYN29aVWfzlwJp9T0rPKEf/dO2QCPAUc0Kq1Tj1GJxo2A==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.9", + "@algolia/autocomplete-shared": "1.19.9" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.9.tgz", + "integrity": "sha512-6mExC6X7762s2SV3eJy3QOkB8bdMmnUhQ2agvGVDuzwoGyr3PquGSY/0vPQXCfiAiCaXUz1rXn+lwghgSi0l0w==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.9" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.9.tgz", + "integrity": "sha512-YosP9Uoek6y/Ur1r1qeogk4biMe/hzkyNcgMCciw0//3XpCM7VlYLSHnyt/vOnEOGhCCc0+3v+unEiH6zz+Z1A==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.56.0.tgz", + "integrity": "sha512-7r4Z3NC7yU1oAQVWJNA2HX7tX481F3pJvCGyLIXiTdBcthz4Q/o21jwcMYDFkuI92UWTNBQQmHYgwHo1zS5dzg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.56.0.tgz", + "integrity": "sha512-avmjXQSq+jadFO8Xl2em05/uQdQnEmHsJyOAdVbZkmVgpMfxL12aJwVVfGNwYr9nulcpuJN1X0lTaQ5wxuNGcA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.56.0.tgz", + "integrity": "sha512-v2TPStUhY//ripPjIVclZ8AWc7DEGooXULZGFlFu37zNatgHjw34oZZ+OSbbc/YHO+xZwPl62I1k8xH1m4S2eg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.56.0.tgz", + "integrity": "sha512-P0ehROpM4Sem3Sqo5x2cKPgj67D3G3jy0rh1Amwkcvsfr6tkvIcdCmerieanqTF7NxUMPNFLkpIFeMO8Rpa50w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.56.0.tgz", + "integrity": "sha512-SXK3Vn3WVxyzbm31oePZBJkp1wpOyuWdd4B/Pv7n0aXDxmeSWhC1R1FC1517mMrFAIaPH4Rt0x6RUe7ZNjz8FA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.56.0.tgz", + "integrity": "sha512-5+ZdX8garFnmycnZgKhtXHePEaLj5zqDxI/0lkhhluzCcvTn0/PvvTirTg8hHYetQHvn7GDyeAiqTAieMvMW4A==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.56.0.tgz", + "integrity": "sha512-+mKUdYvqOi0BcvpAEyCEw49vSBptufIcfibtHz2bdr1pI789M46Yt0uQEk/sxtK3teh71OQvVFHaTDzShUWewQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/events": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", + "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", + "license": "MIT" + }, + "node_modules/@algolia/ingestion": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.56.0.tgz", + "integrity": "sha512-9g/zj+AZx5moFcdFIrYQoVrueXivjUcc3MQHtCYT8WhIuk1lUh1AyEhvJCS0XBZld09cLvd1AZ3BvDBpVpX2UA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.56.0.tgz", + "integrity": "sha512-Qf3Sr6f9A9uxCZUf3MXS0d2b877uYzEB5yxqpVGXAhcJnBCQjrRRon0KvefpGkxy+BshrIJs96OUoMtGqXTFDA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.56.0.tgz", + "integrity": "sha512-GXWG1rWc5wu8hY4N33Y3b6ernY6sAdAvmKWN/zHAiACOx40WnpG0TVX5YazCAr/9gOYGInSiM2A0y2jy2xbiDA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.56.0.tgz", + "integrity": "sha512-7t24cBxaInS3mZb7ddEaZT/tp6q+/aR4YttsQVyP1/i+LmwPR34atO35KjaLFCcRVrlP7sYOAqkCfg6lIRB+ew==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.56.0.tgz", + "integrity": "sha512-R7ePHgVYmDFjZpvrsVAfbDz/d4RxKAYZ5/vgLfIsCVRZRryjWl/3INOxpOICzitehQ5FjNtNjcLQTrmHPTcHBQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.56.0.tgz", + "integrity": "sha512-PIOUXlSnrqM0S+WOgDRb4RzotydJH7ZoT6tOyL7tAO7qJOfvX5wsEW8Pe+PMKMwvuI4/gIyK9cg2H7lJXqnc4Q==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz", + "integrity": "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.29.7.tgz", + "integrity": "sha512-J0wGhKan+rIiE2OhfhRptySLrJ6SjQYM6b6N1FMlhyhCcw1Mig8vQjWchyB+bgHGDvaWo6Diu6CLRMra2uMtmg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz", + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", + "integrity": "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz", + "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz", + "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-react-display-name": "^7.29.7", + "@babel/plugin-transform-react-jsx": "^7.29.7", + "@babel/plugin-transform-react-jsx-development": "^7.29.7", + "@babel/plugin-transform-react-pure-annotations": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", + "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/postcss-alpha-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz", + "integrity": "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", + "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz", + "integrity": "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-function-display-p3-linear": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz", + "integrity": "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-function": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz", + "integrity": "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz", + "integrity": "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-content-alt-text": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz", + "integrity": "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-contrast-color-function": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz", + "integrity": "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-exponential-functions": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", + "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz", + "integrity": "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz", + "integrity": "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz", + "integrity": "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz", + "integrity": "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz", + "integrity": "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz", + "integrity": "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", + "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz", + "integrity": "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz", + "integrity": "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz", + "integrity": "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz", + "integrity": "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", + "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", + "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", + "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz", + "integrity": "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.1.tgz", + "integrity": "sha512-TQUGBuRvxdc7TgNSTevYqrL8oItxiwPDixk20qCB5me/W8uF7BPbhRrAvFuhEoywQp/woRsUZ6SJ+sU5idZAIA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz", + "integrity": "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-position-area-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-position-area-property/-/postcss-position-area-property-1.0.0.tgz", + "integrity": "sha512-fUP6KR8qV2NuUZV3Cw8itx0Ep90aRjAZxAEzC3vrl6yjFv+pFsQbR18UuQctEKmA72K9O27CoYiKEgXxkqjg8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz", + "integrity": "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-property-rule-prelude-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-property-rule-prelude-list/-/postcss-property-rule-prelude-list-1.0.0.tgz", + "integrity": "sha512-IxuQjUXq19fobgmSSvUDO7fVwijDJaZMvWQugxfEUxmjBeDCVaDuMpsZ31MsTm5xbnhA+ElDi0+rQ7sQQGisFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-random-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", + "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz", + "integrity": "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz", + "integrity": "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-sign-functions": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", + "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", + "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-syntax-descriptor-syntax-production": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-syntax-descriptor-syntax-production/-/postcss-syntax-descriptor-syntax-production-1.0.1.tgz", + "integrity": "sha512-GneqQWefjM//f4hJ/Kbox0C6f2T7+pi4/fqTqOFGTL3EjnvOReTqO1qUQ30CaUjkwjYq9qZ41hzarrAxCc4gow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-system-ui-font-family": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-system-ui-font-family/-/postcss-system-ui-font-family-1.0.0.tgz", + "integrity": "sha512-s3xdBvfWYfoPSBsikDXbuorcMG1nN1M6GdU0qBsGfcmNR0A/qhloQZpTxjA3Xsyrk1VJvwb2pOfiOT3at/DuIQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz", + "integrity": "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", + "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz", + "integrity": "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/utilities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", + "integrity": "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docsearch/core": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/core/-/core-4.7.0.tgz", + "integrity": "sha512-p/9xVKmPDj3FPvMfPf5naVO3Ej8SCbcUugGvx1+8GgkuBNbqxqN2Irx3WLBv8VY0jH7XpRwKWdlmjXLZsmTLsg==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@docsearch/css": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-4.7.0.tgz", + "integrity": "sha512-Sk5xkdRFeE7PeWjG9l4AfTwdvMfr9wHiwNNCpHXT4v4SNyNMKdHGvEILc31BgaVFGDDNbv5u/a73tofRiwbEZw==", + "license": "MIT" + }, + "node_modules/@docsearch/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-4.7.0.tgz", + "integrity": "sha512-x6oedjJ8O8/pIDBsMo5Orca3/6cQCz616/CwthVe68l43mqnj2lrJ9kFQITBqy8hMsS3nWeBWFoVO5dJ1DCFKA==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.19.2", + "@docsearch/core": "4.7.0", + "@docsearch/css": "4.7.0" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 20.0.0", + "react": ">= 16.8.0 < 20.0.0", + "react-dom": ">= 16.8.0 < 20.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-core": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz", + "integrity": "sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.19.2", + "@algolia/autocomplete-shared": "1.19.2" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz", + "integrity": "sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.19.2" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/autocomplete-shared": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz", + "integrity": "sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@docusaurus/babel": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.10.2.tgz", + "integrity": "sha512-aJ1hpGyvfkte3dDAfNbWM4biW4yWZBVz7TIGLZP+v+tWOBgxX3e0N5ZIXHIvmfNNXTI77pcHUx3KmtOk05Ze3Q==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/preset-env": "^7.25.9", + "@babel/preset-react": "^7.25.9", + "@babel/preset-typescript": "^7.25.9", + "@babel/runtime": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@docusaurus/logger": "3.10.2", + "@docusaurus/utils": "3.10.2", + "babel-plugin-dynamic-import-node": "^2.3.3", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/bundler": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.10.2.tgz", + "integrity": "sha512-i0ZNcy0f0WhaOlYVgzLsWhIoEXO9kS3HRoKPtgE6vQtZUq7arKZaYdNBudr3mqCmd+TyOkwtwfHgs1ENj07r5g==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.9", + "@docusaurus/babel": "3.10.2", + "@docusaurus/cssnano-preset": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "babel-loader": "^9.2.1", + "clean-css": "^5.3.3", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.11.0", + "css-minimizer-webpack-plugin": "^5.0.1", + "cssnano": "^6.1.2", + "file-loader": "^6.2.0", + "html-minifier-terser": "^7.2.0", + "mini-css-extract-plugin": "^2.9.2", + "null-loader": "^4.0.1", + "postcss": "^8.5.4", + "postcss-loader": "^7.3.4", + "postcss-preset-env": "^10.2.1", + "terser-webpack-plugin": "^5.3.9", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "webpack": "^5.95.0", + "webpackbar": "^7.0.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/faster": "*" + }, + "peerDependenciesMeta": { + "@docusaurus/faster": { + "optional": true + } + } + }, + "node_modules/@docusaurus/core": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.10.2.tgz", + "integrity": "sha512-EYByj6nk+aD9KeVxV6Hmo2/nAAT79P21Y82ycTBOBtrmqilloIbIEhgL2/8Xpt2Jz/pgNqHAwyusOGwmbKeJmA==", + "license": "MIT", + "dependencies": { + "@docusaurus/babel": "3.10.2", + "@docusaurus/bundler": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "boxen": "^6.2.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cli-table3": "^0.6.3", + "combine-promises": "^1.1.0", + "commander": "^5.1.0", + "core-js": "^3.31.1", + "detect-port": "^2.1.0", + "escape-html": "^1.0.3", + "eta": "^2.2.0", + "eval": "^0.1.8", + "execa": "^5.1.1", + "fs-extra": "^11.1.1", + "html-tags": "^3.3.1", + "html-webpack-plugin": "^5.6.0", + "leven": "^3.1.0", + "lodash": "^4.17.21", + "open": "^8.4.0", + "p-map": "^4.0.0", + "prompts": "^2.4.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", + "react-loadable-ssr-addon-v5-slorber": "^1.0.3", + "react-router": "^5.3.4", + "react-router-config": "^5.1.1", + "react-router-dom": "^5.3.4", + "semver": "^7.5.4", + "serve-handler": "^6.1.7", + "tinypool": "^1.0.2", + "tslib": "^2.6.0", + "update-notifier": "^6.0.2", + "webpack": "^5.95.0", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-dev-server": "^5.2.2", + "webpack-merge": "^6.0.1" + }, + "bin": { + "docusaurus": "bin/docusaurus.mjs" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/faster": "*", + "@mdx-js/react": "^3.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@docusaurus/faster": { + "optional": true + } + } + }, + "node_modules/@docusaurus/cssnano-preset": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.10.2.tgz", + "integrity": "sha512-4gCnHRbJLTloiwfvFAa92tgb2gI4KYhvjfQVYnEaiMO/EgvWfCo1LwytHXen+1oZAN0VAlS0JAPxp3MsvKDa3A==", + "license": "MIT", + "dependencies": { + "cssnano-preset-advanced": "^6.1.2", + "postcss": "^8.5.4", + "postcss-sort-media-queries": "^5.2.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/faster": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/faster/-/faster-3.10.2.tgz", + "integrity": "sha512-p/5E5/RyHv+QWusJMPN5i3OMJTqTgkhuwzVbB1AReDWTUHXQCmf5mlTFzGiDrWeQWIDOKsuOPn1jJh0s9LUOHA==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.10.2", + "@rspack/core": "^1.7.10", + "@swc/core": "^1.15.40", + "@swc/html": "^1.15.40", + "browserslist": "^4.24.2", + "lightningcss": "^1.27.0", + "semver": "^7.5.4", + "swc-loader": "^0.2.6", + "tslib": "^2.6.0", + "webpack": "^5.95.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/types": "*" + } + }, + "node_modules/@docusaurus/logger": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.10.2.tgz", + "integrity": "sha512-gSEwqtPfCAnC3ZSJY6xL7tcIfgg0vFD39jbv93eakuweyvO2864xR0K+kmKwBhkTCtWRNjuGGnb5rdmkD/ndqw==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/mdx-loader": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.10.2.tgz", + "integrity": "sha512-9Fd4V/SFjfrVQ0JH5EN0+iPWyFunvTeQE3gfyFeetqPaXMP0OylIjOw16dCuXG4NZJrYdBqwzjh18/h3gRi47w==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "@mdx-js/mdx": "^3.0.0", + "@slorber/remark-comment": "^1.0.0", + "escape-html": "^1.0.3", + "estree-util-value-to-estree": "^3.0.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "image-size": "^2.0.2", + "mdast-util-mdx": "^3.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-raw": "^7.0.0", + "remark-directive": "^3.0.0", + "remark-emoji": "^4.0.0", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.0", + "stringify-object": "^3.3.0", + "tslib": "^2.6.0", + "unified": "^11.0.3", + "unist-util-visit": "^5.0.0", + "url-loader": "^4.1.1", + "vfile": "^6.0.1", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/module-type-aliases": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.10.2.tgz", + "integrity": "sha512-h/I5e4jaAhDHW4vaLENi1i2hnOEnXY1t9R+nnRTbgUl7ymVRzN/HF7dDfj8rKYGj8gfIge+Ef+iYRAMtbGvsrQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.10.2", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "@types/react-router-dom": "*", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@docusaurus/plugin-client-redirects": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.10.2.tgz", + "integrity": "sha512-z5I5ttCXw+8y2gHVZvqAMmUw4Rb0ZzKA5eCPk87SfM/jCKOTvE24yDpPAwwipvug96ij7mOwEHXWw8G4LlWdGA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "eta": "^2.2.0", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-blog": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.10.2.tgz", + "integrity": "sha512-0cbEnNKf0InmLkhj/+nVRmqEnWEoOE8Mh+2x1qOXI0qYpCnphq4RXknVJ8BvybKRXqYVvbmdMfiJSup+k4tm5w==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/theme-common": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "cheerio": "1.0.0-rc.12", + "combine-promises": "^1.1.0", + "feed": "^4.2.2", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "srcset": "^4.0.0", + "tslib": "^2.6.0", + "unist-util-visit": "^5.0.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-docs": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.2.tgz", + "integrity": "sha512-Sqwl4FPoZBDrlY8I2VU2H8O0M91CHp9T8ToMSkTZmjvHCif+1laqfXi6sTk8IfyVS/trN5yNjcWd1bFsGB6W5Q==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/theme-common": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "@types/react-router-config": "^5.0.7", + "combine-promises": "^1.1.0", + "fs-extra": "^11.1.1", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "schema-dts": "^1.1.2", + "tslib": "^2.6.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-content-pages": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.10.2.tgz", + "integrity": "sha512-h5R12sZ/vV9EPiVjvIl9YFCOwkpwXes7dQMYt3EvP6Pphu4amHxxTqWxf08Fl5DR8h+oZMbWpFTNw5vKEYfvzQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-css-cascade-layers": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.10.2.tgz", + "integrity": "sha512-UkdvQby5OQUKWrw3lLnSTJXQ6VETaUVTuPQX9AABtmFm5h+ifEBx1OQ+LN726Q4byuwBf2ElHkf4qU4hTxdvRg==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/plugin-debug": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.10.2.tgz", + "integrity": "sha512-8vbZNOSCpnsT57EY6CgN7sgRVmx3KTYwO8Uvo2pbxOyb8tbqAwtT9SslqaQ41HbA1v1hpn5RP7u5s2KvRwAFpQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "fs-extra": "^11.1.1", + "react-json-view-lite": "^2.3.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-analytics": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.10.2.tgz", + "integrity": "sha512-kMHMBK9j4VAtgd5owwrRLRIi0EjkrpXlX7ePj1+y68XfVZV9I1T4S+koPDm+Hfw2TtnyHvh0uNrDvjz+DjQGVA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-gtag": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.10.2.tgz", + "integrity": "sha512-Vt90nNFhtAChRe9+it1hcHFgFvETdSnOkL5Bma+p6E/yU2tAYrvvyk+gv+LJGM2ZUkyKuKXLRsZ2Lb0bO7+Vog==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-google-tag-manager": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.10.2.tgz", + "integrity": "sha512-MLCffCldysi/R0nzJQP7ZWd0xAoGNnSTiVOo6TTR6mKVGFhE+/XArGe67ZcaZv1uytgQXoXs92VJrgVDrz80rQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-sitemap": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.10.2.tgz", + "integrity": "sha512-PODkwg5XetLML3hU/3xpCKJUZ9cqExLaBnD/Fzzwj2VHogLeqnDisLIujae87zuze7T4mCm2A6KEqZkyiz07EQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "fs-extra": "^11.1.1", + "sitemap": "^7.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/plugin-svgr": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.10.2.tgz", + "integrity": "sha512-JgfT3jWM0TJ8Uw0cEcqxHpybngQY1vlBYpuuNO+gEh5iPh5Ar+vxq/u9CFrYsWeXy48BN7Db76Pzp2edNXUQ8A==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "@svgr/core": "8.1.0", + "@svgr/webpack": "^8.1.0", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/preset-classic": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.10.2.tgz", + "integrity": "sha512-a4B3VczmDl99zK0EufDQYomdJ186WDingjmDXxhN2PNPS9Ty/Y2M5CLFX1KQMRKqRTLiRDKfutzG5IY1FC/ceg==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/plugin-content-blog": "3.10.2", + "@docusaurus/plugin-content-docs": "3.10.2", + "@docusaurus/plugin-content-pages": "3.10.2", + "@docusaurus/plugin-css-cascade-layers": "3.10.2", + "@docusaurus/plugin-debug": "3.10.2", + "@docusaurus/plugin-google-analytics": "3.10.2", + "@docusaurus/plugin-google-gtag": "3.10.2", + "@docusaurus/plugin-google-tag-manager": "3.10.2", + "@docusaurus/plugin-sitemap": "3.10.2", + "@docusaurus/plugin-svgr": "3.10.2", + "@docusaurus/theme-classic": "3.10.2", + "@docusaurus/theme-common": "3.10.2", + "@docusaurus/theme-search-algolia": "3.10.2", + "@docusaurus/types": "3.10.2" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-classic": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.10.2.tgz", + "integrity": "sha512-JqTSLQmqmA9uKWZsD5iwBGJ4JyKB4/yTw6PsSXVPRJG/6GAm/u+add9Iip+hvwP12/AnPNztrdxsI14NJW4KeA==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/plugin-content-blog": "3.10.2", + "@docusaurus/plugin-content-docs": "3.10.2", + "@docusaurus/plugin-content-pages": "3.10.2", + "@docusaurus/theme-common": "3.10.2", + "@docusaurus/theme-translations": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "copy-text-to-clipboard": "^3.2.0", + "infima": "0.2.0-alpha.45", + "lodash": "^4.17.21", + "nprogress": "^0.2.0", + "postcss": "^8.5.4", + "prism-react-renderer": "^2.3.0", + "prismjs": "^1.29.0", + "react-router-dom": "^5.3.4", + "rtlcss": "^4.1.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-common": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.10.2.tgz", + "integrity": "sha512-R9b/vMpK1yye6hNZTA6x/ivRv+at6GhxnXcxkpzCGzO1R1RwiquqiFg2wMFh6aqlJTpWRFKpFD2TzCDQcyOU0A==", + "license": "MIT", + "dependencies": { + "@docusaurus/mdx-loader": "3.10.2", + "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router-config": "*", + "clsx": "^2.0.0", + "parse-numeric-range": "^1.3.0", + "prism-react-renderer": "^2.3.0", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-search-algolia": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.10.2.tgz", + "integrity": "sha512-1msxllyhi/5m77JukXtp5UFnUAriwZIC1oJ7MTnpQpCwLTbclJi5BK5n28CTZuSXpQN2ewbbnqRgAhMM6c6ihg==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "^1.19.2", + "@docsearch/react": "^3.9.0 || ^4.3.2", + "@docusaurus/core": "3.10.2", + "@docusaurus/logger": "3.10.2", + "@docusaurus/plugin-content-docs": "3.10.2", + "@docusaurus/theme-common": "3.10.2", + "@docusaurus/theme-translations": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-validation": "3.10.2", + "algoliasearch": "^5.37.0", + "algoliasearch-helper": "^3.26.0", + "clsx": "^2.0.0", + "eta": "^2.2.0", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "tslib": "^2.6.0", + "utility-types": "^3.10.0" + }, + "engines": { + "node": ">=20.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/theme-translations": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.10.2.tgz", + "integrity": "sha512-iv20wrxnyXkY89LM3TzRlzGlt5fIGO5UnaR6UL1ZVfB9RRFjxQFQ6awDrwAc6Km8Y5gD8pInuwYPF+6/TiCxXA==", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/types": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.10.2.tgz", + "integrity": "sha512-B6rvfwIFSapUqUJjMriZswX13K8l5Z7AcmVE6uTEJpYddQieSTR12DsGaFtcZAIDsQd4p+0WTl0Vc6jmZK0Trw==", + "license": "MIT", + "dependencies": { + "@mdx-js/mdx": "^3.0.0", + "@types/history": "^4.7.11", + "@types/mdast": "^4.0.2", + "@types/react": "*", + "commander": "^5.1.0", + "joi": "^17.9.2", + "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", + "utility-types": "^3.10.0", + "webpack": "^5.95.0", + "webpack-merge": "^5.9.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@docusaurus/types/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docusaurus/utils": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.10.2.tgz", + "integrity": "sha512-xx0W3eav2uW1NRIpuHJWNwLTC15xPNjU4Uxi9NSnd3swYC96BE3vFiT93SD8s24kmAAWNwgZwfZ2fghGZ01Lcw==", + "license": "MIT", + "dependencies": { + "@11ty/gray-matter": "^1.0.0", + "@docusaurus/logger": "3.10.2", + "@docusaurus/types": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "escape-string-regexp": "^4.0.0", + "execa": "^5.1.1", + "file-loader": "^6.2.0", + "fs-extra": "^11.1.1", + "github-slugger": "^1.5.0", + "globby": "^11.1.0", + "jiti": "^1.20.0", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "micromatch": "^4.0.5", + "p-queue": "^6.6.2", + "prompts": "^2.4.2", + "resolve-pathname": "^3.0.0", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-common": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.10.2.tgz", + "integrity": "sha512-x3Dz6jv6iQKBNjBmVTu8p57abMp/VNTUgKBMgRVXJc5444orBTsArv0+cdfrXTiz/VMmHfDRVkPbL7GH2B7T7w==", + "license": "MIT", + "dependencies": { + "@docusaurus/types": "3.10.2", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@docusaurus/utils-validation": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.10.2.tgz", + "integrity": "sha512-sn8unbDfUL585NtR3cwHefPicOyaHvPaX7VD0aOg/siIxUBoKyKKaGEqzJZDS64mM43TnxurkYDtmB1wsJlZsw==", + "license": "MIT", + "dependencies": { + "@docusaurus/logger": "3.10.2", + "@docusaurus/utils": "3.10.2", + "@docusaurus/utils-common": "3.10.2", + "fs-extra": "^11.2.0", + "joi": "^17.9.2", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=20.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/buffers": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz", + "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/codegen": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", + "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-core": { + "version": "4.68.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.68.0.tgz", + "integrity": "sha512-OAioDU3UGV34ECVTB4pt641OfUzuyDuyUmgrlVM+Fp9g4YyavZrB4xxWuupYrU66+81+um9D0DAzYhjXto5ysQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.68.0", + "@jsonjoy.com/fs-node-utils": "4.68.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-fsa": { + "version": "4.68.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.68.0.tgz", + "integrity": "sha512-dC6VeW8uqXMF9Hkqb7qYST/t165XCHG+bNwwMzRetoNZRxJC8anr5XCJSAk5B+YLHGn6SP+noNltU6OFg/oa7A==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.0", + "@jsonjoy.com/fs-node-builtins": "4.68.0", + "@jsonjoy.com/fs-node-utils": "4.68.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node": { + "version": "4.68.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.68.0.tgz", + "integrity": "sha512-xYDlpSk3UDHjcI0kiz6kNJ/oVTEzlS7kUzAoAsgJh2/+yOtYV/eZW7M8O9Fj+ED3BDONYKt7+8OiPlWl6xpFeA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.0", + "@jsonjoy.com/fs-node-builtins": "4.68.0", + "@jsonjoy.com/fs-node-utils": "4.68.0", + "@jsonjoy.com/fs-print": "4.68.0", + "@jsonjoy.com/fs-snapshot": "4.68.0", + "glob-to-regex.js": "^1.0.0", + "thingies": "^2.5.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-builtins": { + "version": "4.68.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.68.0.tgz", + "integrity": "sha512-j3bb+k4NuqpqXQzinCLeagS7LGLCUvPqA6TnpaBkBqhnPjbzETYiQFpD7pWaV8cGOhhZcAv9dmpzk5mxNb1qsg==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-to-fsa": { + "version": "4.68.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.68.0.tgz", + "integrity": "sha512-dtLpmLQxw3IBcGxoK9tVvftwmQLl9qmQyivt8DZpeEwDUcQkxk2CLMZERXtmOlF1mWXfT9CqtjjMiswkb03rKw==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-fsa": "4.68.0", + "@jsonjoy.com/fs-node-builtins": "4.68.0", + "@jsonjoy.com/fs-node-utils": "4.68.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-node-utils": { + "version": "4.68.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.68.0.tgz", + "integrity": "sha512-Q84pogSfRaz0WNrBBy+HzBmhNu4m6tWoM0po8bLwW4pKvrwrSadOgFpm1sku08sfg0QAtUubkWuZEBDpNI3toQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-builtins": "4.68.0", + "glob-to-regex.js": "^1.0.1" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-print": { + "version": "4.68.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.68.0.tgz", + "integrity": "sha512-sdy9F6N9QEcyAcU8OxEQmB91mlgiyuMphTFeijQ+qFgigCOeT16lW3X1dCWNzD7wwKLOKXnnYqsFyeq4LWSlYQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-node-utils": "4.68.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot": { + "version": "4.68.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.68.0.tgz", + "integrity": "sha512-eV44t9KY9LH47aPR1a4Nv4xYxM1vQ+OKEw9Mx/3zkuxGOILwmtFddlh5HakdMFpFHwuFNPw1s+NGq2eVYFpQRA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^17.65.0", + "@jsonjoy.com/fs-node-utils": "4.68.0", + "@jsonjoy.com/json-pack": "^17.65.0", + "@jsonjoy.com/util": "^17.65.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz", + "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz", + "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz", + "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "17.67.0", + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0", + "@jsonjoy.com/json-pointer": "17.67.0", + "@jsonjoy.com/util": "17.67.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz", + "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/util": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { + "version": "17.67.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz", + "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "17.67.0", + "@jsonjoy.com/codegen": "17.67.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", + "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/buffers": "^1.2.0", + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/json-pointer": "^1.0.2", + "@jsonjoy.com/util": "^1.9.0", + "hyperdyperid": "^1.2.0", + "thingies": "^2.5.0", + "tree-dump": "^1.1.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pointer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", + "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/codegen": "^1.0.0", + "@jsonjoy.com/util": "^1.9.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", + "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/buffers": "^1.0.0", + "@jsonjoy.com/codegen": "^1.0.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", + "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "license": "MIT" + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", + "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "acorn": "^8.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@module-federation/error-codes": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", + "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", + "license": "MIT" + }, + "node_modules/@module-federation/runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", + "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/runtime-core": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-core": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", + "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-tools": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", + "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/webpack-bundler-runtime": "0.22.0" + } + }, + "node_modules/@module-federation/sdk": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", + "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", + "license": "MIT" + }, + "node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", + "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz", + "integrity": "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "@peculiar/asn1-x509-attr": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.8.0.tgz", + "integrity": "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.8.0.tgz", + "integrity": "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.8.0.tgz", + "integrity": "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.8.0", + "@peculiar/asn1-pkcs8": "^2.8.0", + "@peculiar/asn1-rsa": "^2.8.0", + "@peculiar/asn1-schema": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.8.0.tgz", + "integrity": "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.8.0.tgz", + "integrity": "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.8.0", + "@peculiar/asn1-pfx": "^2.8.0", + "@peculiar/asn1-pkcs8": "^2.8.0", + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "@peculiar/asn1-x509-attr": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.8.0.tgz", + "integrity": "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz", + "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==", + "license": "MIT", + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.8.0.tgz", + "integrity": "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.8.0.tgz", + "integrity": "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.8.0", + "@peculiar/asn1-x509": "^2.8.0", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.3.tgz", + "integrity": "sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==", + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@rspack/binding": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.12.tgz", + "integrity": "sha512-f4HHuLbvuld8Ba4iB/4ibse5XrKxFrgmM3S4P2AOKnPlekAFlBjmltCuaTL/W2ggYvILaVY+YcFXrEH1rrKeQA==", + "license": "MIT", + "optionalDependencies": { + "@rspack/binding-darwin-arm64": "1.7.12", + "@rspack/binding-darwin-x64": "1.7.12", + "@rspack/binding-linux-arm64-gnu": "1.7.12", + "@rspack/binding-linux-arm64-musl": "1.7.12", + "@rspack/binding-linux-x64-gnu": "1.7.12", + "@rspack/binding-linux-x64-musl": "1.7.12", + "@rspack/binding-wasm32-wasi": "1.7.12", + "@rspack/binding-win32-arm64-msvc": "1.7.12", + "@rspack/binding-win32-ia32-msvc": "1.7.12", + "@rspack/binding-win32-x64-msvc": "1.7.12" + } + }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.12.tgz", + "integrity": "sha512-rbFprJaJiqrmfy8SHth8EsoRS0wg4bXcucwj9NiMzpGFq14Opw8c04iQ6H9BECYzgmN0PKZ9rh41LdVvhdZe4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.12.tgz", + "integrity": "sha512-jnOp+/UXOJa9xqUb8KXH03sysoO2e4Ij6tw6MqDdmdj8n/A8PQENRPUbW9AwXpPtVDJPus9r4fi7b3+6e4B8Hg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.12.tgz", + "integrity": "sha512-C8owWG+yvo7X0oVLIXetkoJhIFBP1LYNcAQqtgLmJnQLQDklGuP83dKC+zISGQWpjawHfZ1ER96vLgoTrxKZdw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.12.tgz", + "integrity": "sha512-i51WWI64aRpsfSki6rN0aepPqXkVfS+vZM7+4bWDcmnhUmdMvhIPcYg0QRk3DtyJnu33jqNLM0WHY78k00NyfA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.12.tgz", + "integrity": "sha512-MSos0FuPEefqo9V92ULd5hggKG29EkSNg1zDcypy0OkpsKh5pfjVxTLYFXgTcVyFoUQQbdG8zFBzYbwmJ8V4ew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.12.tgz", + "integrity": "sha512-JcAMVKXOnjfpC3coWjCFPWD3Yl8RBw6a+IXQQ8mfRlHaHMIiOv8IfZqx15XRxMUn49CtP7Z0Na8iiAg2aKrcfw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-wasm32-wasi": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.12.tgz", + "integrity": "sha512-n+ZqP6ZMc0nhOgvadg5VhEs9ojtbES80AcWeFnmGkbzIszvGSO63GKNiRkXtjJ9KFuRzytbbmsCqkUVH+Tywxg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "1.0.7" + } + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.12.tgz", + "integrity": "sha512-8+h5fYDXYdmugbdfZ+D1y8IQ3rv2EhSfyGP7vBe+bjNyaMa4jWrpucmZbtxojUL1AzaeuHbvMdj9UO/gelk/+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.12.tgz", + "integrity": "sha512-cDMGwTRSa2p9fNBVe1wTRkF2AEXZ9ARWW36QeC5CkLaI0Ezz8lvhF2+CSOPnhaQ1O1qtn0L0SF+lFnrY+I7xGQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.12.tgz", + "integrity": "sha512-wIqFvlgFqrgUyj/6S/FJcvShnkZOmIeXTfqvheLY67MGq8qd8jb1YimQVKAIrmWB3yuJKUFACI3Ag1UBtEedEA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/core": { + "version": "1.7.12", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.12.tgz", + "integrity": "sha512-6CwFIHlhRmXfZoMj3v9MZ1SMTPBn+cHVXeMIeaGp5sufqinKsISbsqHu6ZMJu2wDSmZLdmQJX6zLxkhcAUlhkQ==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime-tools": "0.22.0", + "@rspack/binding": "1.7.12", + "@rspack/lite-tapable": "1.1.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.1" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@rspack/lite-tapable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz", + "integrity": "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==", + "license": "MIT" + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@slorber/remark-comment": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz", + "integrity": "sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==", + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.1.0", + "micromark-util-symbol": "^1.0.1" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@swc/core": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.47.tgz", + "integrity": "sha512-FbsO5JcfOjfH38W/rohBRBweJeERsAuIP4f377lmkmxTcq9exjtx4SkRuZY5CdfhR2CBVwDIJegBpJDffwNsOg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.27" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.47", + "@swc/core-darwin-x64": "1.15.47", + "@swc/core-linux-arm-gnueabihf": "1.15.47", + "@swc/core-linux-arm64-gnu": "1.15.47", + "@swc/core-linux-arm64-musl": "1.15.47", + "@swc/core-linux-ppc64-gnu": "1.15.47", + "@swc/core-linux-s390x-gnu": "1.15.47", + "@swc/core-linux-x64-gnu": "1.15.47", + "@swc/core-linux-x64-musl": "1.15.47", + "@swc/core-win32-arm64-msvc": "1.15.47", + "@swc/core-win32-ia32-msvc": "1.15.47", + "@swc/core-win32-x64-msvc": "1.15.47" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.47.tgz", + "integrity": "sha512-GsoMtan3ojGGMGFbl31mmRu5ctZ56re8grGE8mO/OHJ8O+JRkzod02fe7X6ZQ8JvamA3imkEkx/h3u+vsOgPgA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.47.tgz", + "integrity": "sha512-leTi7Rx3KF4zcC637iqWgk9SoV8VXAD8ppQYXsep63px5A/UftOcxLN1pmr8Z1si/YvX90ompP/rHgpYkgwXWg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.47.tgz", + "integrity": "sha512-hBqHuoWKKIsKmDBn9qVeWqj5GWZhtlcczVaqQmNRXsDfq+voR5CxKRfamA367QjJXtceYuliLFfEL8QsskRM2g==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.47.tgz", + "integrity": "sha512-TBxvRz+B4K205TWHHZxWVxkC2RFNP/Mz3PNcECBos5PsKwxjg3QSJzdoebr0VCf0Bfh8HOPldKxAP/8XkFe9gA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.47.tgz", + "integrity": "sha512-3Yu3Uq/VgytqsPjTMbkPU1ExADytbdWbruJYhA584E9jrpE2Ki+R6VVPoZCeAVk1Cb7QxcRTgblw6bSa6a/R+w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.47.tgz", + "integrity": "sha512-wfdMi5IaOaNtmh2/6geRoxIdNfqylUZFdtzTKS655y1axWfIWyx7As74vv0wVdjeCIZ3WmCI9odDd4rUttXOSQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.47.tgz", + "integrity": "sha512-3hHYBY0yx8Ez7GMRrkhXHQzMdR5IZA6Wq5Ee4svlgwvSECLpnAJ9+0AimEGUFDvuLwE7nV/2+PYe8+Nm4rvNcQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.47.tgz", + "integrity": "sha512-TjfhjgP/jGCfFHYC3JQPhJA1HwErbIJ9JfREDc1KNkvY6P0LodCgKVIlQ5deeTbkG7ih3bF5PHJLuLpaZjdRyQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.47.tgz", + "integrity": "sha512-CQpS8Ge/avfjZd0UEwG/sds83Uu32deQXcV1Jo3jD0mmvQQqtYAjpsDZXugmheeAwmt+YIuoVtVHro8LMYHqsQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.47.tgz", + "integrity": "sha512-0W8IKHsUTYiT7G2RqtOoVWk+89yzZikIiDUb/sCK6BmQDBhN91hQSfyUtW12jhEWLzYgcfmisfsZrmZE+84U1A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.47.tgz", + "integrity": "sha512-ZIp49d2Z4/ka2jO9otOg4hDvTdPmp86kVOgS2M5FCPI7eKKZ1W0boxWn+8XeZrfERtFGW0AlMRm4JhlJa7l3NA==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.47.tgz", + "integrity": "sha512-2h8Iek95vnixkBRCo+H8p09+Q5ll2NgSMFrWTy0iKt7+/t+8/T5mBpiT6c0ZxSS7wcWjwZ9sGZkK70tTSYHdDw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/html": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html/-/html-1.15.47.tgz", + "integrity": "sha512-Olu/8OORvYB+43v2/85nHe1EA8WSvWp8D3kctEAput/o4F+EWCK0nr4yGTqvWB7Z+ODz0k+vnxpvHQYzsKP2LA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@swc/html-darwin-arm64": "1.15.47", + "@swc/html-darwin-x64": "1.15.47", + "@swc/html-linux-arm-gnueabihf": "1.15.47", + "@swc/html-linux-arm64-gnu": "1.15.47", + "@swc/html-linux-arm64-musl": "1.15.47", + "@swc/html-linux-ppc64-gnu": "1.15.47", + "@swc/html-linux-s390x-gnu": "1.15.47", + "@swc/html-linux-x64-gnu": "1.15.47", + "@swc/html-linux-x64-musl": "1.15.47", + "@swc/html-win32-arm64-msvc": "1.15.47", + "@swc/html-win32-ia32-msvc": "1.15.47", + "@swc/html-win32-x64-msvc": "1.15.47" + } + }, + "node_modules/@swc/html-darwin-arm64": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-darwin-arm64/-/html-darwin-arm64-1.15.47.tgz", + "integrity": "sha512-Qe3FXNLO/k2WN7xmBP8SzBrtLvlIbyqa8cceUOI0opE4gk+myk5qIk022MiGJOgiF1B21/wMXgA08gYjQ2lP2Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-darwin-x64": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-darwin-x64/-/html-darwin-x64-1.15.47.tgz", + "integrity": "sha512-khCOiWmqlqi8yz+F/ePExhht/+3/cz1XzKV7raPKVynzd6sPxYtFQlNubj305s2soEXgkYYIH0zVVLwiMOVn6Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-arm-gnueabihf": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-linux-arm-gnueabihf/-/html-linux-arm-gnueabihf-1.15.47.tgz", + "integrity": "sha512-7VRqaOEQEFPHSbUcnobmI59WbFK9M35Sse8TFN3hHxOlK9fINJ9yN5FuE9XEeRQAk+PFdR6WrIcF1Xwx1Cpi8A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-arm64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-gnu/-/html-linux-arm64-gnu-1.15.47.tgz", + "integrity": "sha512-Ri9MmQqVRiEWuRfpkSTqz0bguvejXQcqQAkoW1Hke6+DColfzETcgNWa/Kw/ii5A/SS9Nb7acKaYdIw1vMBPwA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-arm64-musl": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-linux-arm64-musl/-/html-linux-arm64-musl-1.15.47.tgz", + "integrity": "sha512-NyXRQiVBkeutgCwCxUUaFqZdDmpZONs7Zz3AZGoY35s9GwXF412Nt22fK/e7lO/sM6G/+S3rOom/0xTmUQSK6A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-ppc64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-linux-ppc64-gnu/-/html-linux-ppc64-gnu-1.15.47.tgz", + "integrity": "sha512-rQ+XLJHFzu0e4M5p1+8kF2FKzI7WO9KPPji87OuicHZVrDCEqg7/jSGu3hys9CjIQIYVfR+tAFuZz3Q1/jOoNA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-s390x-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-linux-s390x-gnu/-/html-linux-s390x-gnu-1.15.47.tgz", + "integrity": "sha512-w6bitHrllrE3lqmf14cT3spmWhZHGts1O08O4adzxztSPto7O5GM3IerqZm/NtsqlxNsfbVHnhaNXxXWe/8Q+Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-x64-gnu": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-linux-x64-gnu/-/html-linux-x64-gnu-1.15.47.tgz", + "integrity": "sha512-FTA7E29gcyadd71prg8sJUVacYrIhAXS8L7p48yCfgcNOKquofN1TDOrHVOyYMoxplL3FUwwbN+AZxWO7QfWPQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-linux-x64-musl": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-linux-x64-musl/-/html-linux-x64-musl-1.15.47.tgz", + "integrity": "sha512-g+K1GKUrj+S9o8Qsgii2g7ooMzZ750R4IAqH5CVElIA5FTMAx7KGnu9ga6aEnRwwLYxY3s1k/nN+ls0NEk240g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-win32-arm64-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-win32-arm64-msvc/-/html-win32-arm64-msvc-1.15.47.tgz", + "integrity": "sha512-5Iw3i00JdTySi+ccc2CM5bxY+8TZivQmcTqUC6mUdAY0/KYBgSe1/L294UJQ4OTLQqNDOYXY9jdb070zZUX7jg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-win32-ia32-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-win32-ia32-msvc/-/html-win32-ia32-msvc-1.15.47.tgz", + "integrity": "sha512-YkKd0hbIOeuFC1Sg7uGKkU2JhL+c9vAJlT6P0nRbWCAL71n01IPbdtbW6PmvgVmI7pxRYucXpr4pg839nX5+9Q==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/html-win32-x64-msvc": { + "version": "1.15.47", + "resolved": "https://registry.npmjs.org/@swc/html-win32-x64-msvc/-/html-win32-x64-msvc-1.15.47.tgz", + "integrity": "sha512-w9Ug+VB9hlHT4hTWIJSB40AdF+qs5w07CYq/+ef1vQRjT5naN6/SKuXmr9/CKLn3T7A9z3lRMPKE4+lmtUkBOQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/types": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", + "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "license": "MIT" + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-config": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.11.tgz", + "integrity": "sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "^5.1.0" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "license": "MIT" + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/address": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/address/-/address-2.0.3.tgz", + "integrity": "sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/algoliasearch": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.56.0.tgz", + "integrity": "sha512-PrqppUmhT4ENdas2pH9caE7efUcxy6EcSFhWzosiVuQBzu2tQ5yLTI6jwomT/1cuBnivzGfxiJCqDNN9FRRh+Q==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.22.0", + "@algolia/client-abtesting": "5.56.0", + "@algolia/client-analytics": "5.56.0", + "@algolia/client-common": "5.56.0", + "@algolia/client-insights": "5.56.0", + "@algolia/client-personalization": "5.56.0", + "@algolia/client-query-suggestions": "5.56.0", + "@algolia/client-search": "5.56.0", + "@algolia/ingestion": "1.56.0", + "@algolia/monitoring": "1.56.0", + "@algolia/recommend": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/algoliasearch-helper": { + "version": "3.29.3", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.29.3.tgz", + "integrity": "sha512-gVOMbbPVrCO3Xs+B+BLAIGFqIQow6qHMTYCEeBqLQB9m4ZmKUqMwkEumlal2iyyezHEd4Y0FvFTLbCRutCutIQ==", + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "license": "MIT", + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", + "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "license": "MIT" + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bonjour-service": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.4.tgz", + "integrity": "sha512-jCZcVv7eoc4QesRscwEZtSROBen+6LpKAmBIsQYQrsAeVHLyMXWX/t6eIV5KiRZYNUBl8eVqImEEMQ8L5+c/Kw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/boxen": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", + "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^6.2.0", + "chalk": "^4.1.2", + "cli-boxes": "^3.0.0", + "string-width": "^5.0.1", + "type-fest": "^2.5.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001807", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001807.tgz", + "integrity": "sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/combine-promises": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/combine-promises/-/combine-promises-1.2.0.tgz", + "integrity": "sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "license": "ISC" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compressible/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/configstore": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", + "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^6.0.1", + "graceful-fs": "^4.2.6", + "unique-string": "^3.0.0", + "write-file-atomic": "^3.0.3", + "xdg-basedir": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/yeoman/configstore?sponsor=1" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/copy-text-to-clipboard": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.2.tgz", + "integrity": "sha512-T6SqyLd1iLuqPA90J5N4cTalrtovCySh58iiZDGJ6FGznbclKh4UI+FGacQSgFzwKG77W7XT5gwbVEbd9cIH1A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz", + "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==", + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", + "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/css-blank-pseudo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz", + "integrity": "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.4.0.tgz", + "integrity": "sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==", + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-has-pseudo": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz", + "integrity": "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-minimizer-webpack-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", + "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "cssnano": "^6.0.1", + "jest-worker": "^29.4.3", + "postcss": "^8.4.24", + "schema-utils": "^4.0.1", + "serialize-javascript": "^6.0.1" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@parcel/css": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "lightningcss": { + "optional": true + } + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", + "integrity": "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.9.0.tgz", + "integrity": "sha512-J8jOU/hLjaXcO1LldOLraJSQpfLXRKof0I7mtbRyOy2AAXgqst0x9rlgi2qXeD6d0ou3ZLqcPAMqYVbpCbrxEw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-advanced": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", + "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.0", + "cssnano-preset-default": "^6.1.2", + "postcss-discard-unused": "^6.0.5", + "postcss-merge-idents": "^6.0.3", + "postcss-reduce-idents": "^6.0.3", + "postcss-zindex": "^6.0.2" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" + }, + "node_modules/detect-port": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-2.1.0.tgz", + "integrity": "sha512-epZuWb/6Q62L+nDHJc/hQAqf8pylsqgk3BpZXVBx1CDnr3nkrVNn73Uu1rXcFzkNcc+hkP3whuOg7JZYaQB65Q==", + "license": "MIT", + "dependencies": { + "address": "^2.0.1" + }, + "bin": { + "detect": "dist/commonjs/bin/detect-port.js", + "detect-port": "dist/commonjs/bin/detect-port.js" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "license": "MIT", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "license": "MIT", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.402", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz", + "integrity": "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/emoticon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", + "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz", + "integrity": "sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", + "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eval": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", + "dependencies": { + "@types/node": "*", + "require-like": ">= 0.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/express/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "license": "MIT", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/file-loader/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "license": "MIT", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "license": "MIT", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regex.js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", + "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-yarn": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", + "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/history": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", + "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2", + "loose-envify": "^1.2.0", + "resolve-pathname": "^3.0.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0", + "value-equal": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "license": "MIT" + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.8", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.8.tgz", + "integrity": "sha512-MZmKQcTnhEh1SPSyMiEytIeDZDUoBZVorNHivQGXMASHf/BSGGOrKa2xQ5bGx3TCe1n109ecCt+cpww7wwWhKA==", + "license": "MIT", + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x || 2.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/html-webpack-plugin/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz", + "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==", + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "license": "MIT", + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infima": { + "version": "0.2.0-alpha.45", + "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz", + "integrity": "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-network-error": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz", + "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-npm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", + "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-yarn-global": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", + "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.13.4", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz", + "integrity": "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "license": "MIT", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/launch-editor": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.4" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.68.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.68.0.tgz", + "integrity": "sha512-qlU8XrIfXUuZcmXk/GODL47KXXnhSbbaWtW5z+L7cLFfIQbgAOWzL5lqUYn39kfJh+BHLaD/siaM94/UTweGow==", + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/fs-core": "4.68.0", + "@jsonjoy.com/fs-fsa": "4.68.0", + "@jsonjoy.com/fs-node": "4.68.0", + "@jsonjoy.com/fs-node-builtins": "4.68.0", + "@jsonjoy.com/fs-node-to-fsa": "4.68.0", + "@jsonjoy.com/fs-node-utils": "4.68.0", + "@jsonjoy.com/fs-print": "4.68.0", + "@jsonjoy.com/fs-snapshot": "4.68.0", + "@jsonjoy.com/json-pack": "^1.11.0", + "@jsonjoy.com/util": "^1.9.0", + "glob-to-regex.js": "^1.0.1", + "thingies": "^2.5.0", + "tree-dump": "^1.0.3", + "tslib": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-directive": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", + "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-space/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-events-to-acorn/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-normalize-identifier/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark/node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark/node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", + "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/minimizer-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "license": "MIT", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/null-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", + "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/null-loader/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/null-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/null-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/null-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", + "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", + "license": "MIT", + "dependencies": { + "got": "^12.1.0", + "registry-auth-token": "^5.0.1", + "registry-url": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "license": "ISC" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "license": "MIT", + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "license": "BSD-3-Clause", + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", + "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", + "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", + "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", + "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-custom-media": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", + "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-properties": { + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", + "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", + "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", + "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-unused": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", + "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", + "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", + "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-focus-within": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", + "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", + "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-image-set-function": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", + "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-lab-function": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", + "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-logical": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", + "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-merge-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", + "integrity": "sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nesting": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-resolve-nested": "^3.1.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "license": "MIT", + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", + "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", + "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.6.1.tgz", + "integrity": "sha512-yrk74d9EvY+W7+lO9Aj1QmjWY9q5NsKjK2V9drkOPZB/X6KZ0B3igKsHUYakb7oYVhnioWypQX3xGuePf89f3g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-alpha-function": "^1.0.1", + "@csstools/postcss-cascade-layers": "^5.0.2", + "@csstools/postcss-color-function": "^4.0.12", + "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", + "@csstools/postcss-color-mix-function": "^3.0.12", + "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", + "@csstools/postcss-content-alt-text": "^2.0.8", + "@csstools/postcss-contrast-color-function": "^2.0.12", + "@csstools/postcss-exponential-functions": "^2.0.9", + "@csstools/postcss-font-format-keywords": "^4.0.0", + "@csstools/postcss-gamut-mapping": "^2.0.11", + "@csstools/postcss-gradients-interpolation-method": "^5.0.12", + "@csstools/postcss-hwb-function": "^4.0.12", + "@csstools/postcss-ic-unit": "^4.0.4", + "@csstools/postcss-initial": "^2.0.1", + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "@csstools/postcss-light-dark-function": "^2.0.11", + "@csstools/postcss-logical-float-and-clear": "^3.0.0", + "@csstools/postcss-logical-overflow": "^2.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", + "@csstools/postcss-logical-resize": "^3.0.0", + "@csstools/postcss-logical-viewport-units": "^3.0.4", + "@csstools/postcss-media-minmax": "^2.0.9", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", + "@csstools/postcss-nested-calc": "^4.0.0", + "@csstools/postcss-normalize-display-values": "^4.0.1", + "@csstools/postcss-oklab-function": "^4.0.12", + "@csstools/postcss-position-area-property": "^1.0.0", + "@csstools/postcss-progressive-custom-properties": "^4.2.1", + "@csstools/postcss-property-rule-prelude-list": "^1.0.0", + "@csstools/postcss-random-function": "^2.0.1", + "@csstools/postcss-relative-color-syntax": "^3.0.12", + "@csstools/postcss-scope-pseudo-class": "^4.0.1", + "@csstools/postcss-sign-functions": "^1.1.4", + "@csstools/postcss-stepped-value-functions": "^4.0.9", + "@csstools/postcss-syntax-descriptor-syntax-production": "^1.0.1", + "@csstools/postcss-system-ui-font-family": "^1.0.0", + "@csstools/postcss-text-decoration-shorthand": "^4.0.3", + "@csstools/postcss-trigonometric-functions": "^4.0.9", + "@csstools/postcss-unset-value": "^4.0.0", + "autoprefixer": "^10.4.23", + "browserslist": "^4.28.1", + "css-blank-pseudo": "^7.0.1", + "css-has-pseudo": "^7.0.3", + "css-prefers-color-scheme": "^10.0.0", + "cssdb": "^8.6.0", + "postcss-attribute-case-insensitive": "^7.0.1", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^7.0.12", + "postcss-color-hex-alpha": "^10.0.0", + "postcss-color-rebeccapurple": "^10.0.0", + "postcss-custom-media": "^11.0.6", + "postcss-custom-properties": "^14.0.6", + "postcss-custom-selectors": "^8.0.5", + "postcss-dir-pseudo-class": "^9.0.1", + "postcss-double-position-gradients": "^6.0.4", + "postcss-focus-visible": "^10.0.1", + "postcss-focus-within": "^9.0.1", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^6.0.0", + "postcss-image-set-function": "^7.0.0", + "postcss-lab-function": "^7.0.12", + "postcss-logical": "^8.1.0", + "postcss-nesting": "^13.0.2", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^6.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^10.0.0", + "postcss-pseudo-class-any-link": "^10.0.1", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", + "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-reduce-idents": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", + "integrity": "sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", + "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-sort-media-queries": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz", + "integrity": "sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==", + "license": "MIT", + "dependencies": { + "sort-css-media-queries": "2.2.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.4.23" + } + }, + "node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postcss-zindex": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz", + "integrity": "sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==", + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", + "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "license": "ISC" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", + "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0" + }, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "name": "@slorber/react-helmet-async", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", + "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.12.5", + "invariant": "^2.2.4", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.2.0", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-json-view-lite": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", + "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-loadable": { + "name": "@docusaurus/react-loadable", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", + "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-loadable-ssr-addon-v5-slorber": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.3.tgz", + "integrity": "sha512-GXfh9VLwB5ERaCsU6RULh7tkemeX15aNh6wuMEBtfdyMa7fFG8TXrhXlx1SoEK2Ty/l6XIkzzYIQmyaWW3JgdQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.3" + }, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "react-loadable": "*", + "webpack": ">=4.41.1 || 5.x" + } + }, + "node_modules/react-router": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", + "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "hoist-non-react-statics": "^3.1.0", + "loose-envify": "^1.3.1", + "path-to-regexp": "^1.7.0", + "prop-types": "^15.6.2", + "react-is": "^16.6.0", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/react-router-config": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz", + "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + }, + "peerDependencies": { + "react": ">=15", + "react-router": ">=5" + } + }, + "node_modules/react-router-dom": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", + "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13", + "history": "^4.9.0", + "loose-envify": "^1.3.1", + "prop-types": "^15.6.2", + "react-router": "5.3.4", + "tiny-invariant": "^1.0.2", + "tiny-warning": "^1.0.0" + }, + "peerDependencies": { + "react": ">=15" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", + "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^3.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/registry-url": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", + "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", + "license": "MIT", + "dependencies": { + "rc": "1.2.8" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/remark-directive": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", + "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^3.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-emoji": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-4.0.1.tgz", + "integrity": "sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.2", + "emoticon": "^4.0.1", + "mdast-util-find-and-replace": "^3.0.1", + "node-emoji": "^2.1.0", + "unified": "^11.0.4" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "license": "MIT", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/renderkid/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/renderkid/node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-like": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", + "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", + "engines": { + "node": "*" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pathname": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", + "license": "MIT" + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rtlcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", + "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", + "license": "MIT", + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/schema-dts": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", + "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", + "license": "Apache-2.0" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT", + "peer": true + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz", + "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", + "license": "MIT", + "dependencies": { + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", + "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "license": "MIT" + }, + "node_modules/serve-index": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.3.tgz", + "integrity": "sha512-tAjEd+wt/YwnEbfNB2ht51ybBJxbEWwe5ki/Z//Wh0rpBFTCUSj46GnxUKEWzhfuJTsee8x3lybHxFgUMig2hw==", + "license": "MIT", + "dependencies": { + "@types/node": "^17.0.5", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.2.4" + }, + "bin": { + "sitemap": "dist/cli.js" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=5.6.0" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", + "license": "MIT" + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sort-css-media-queries": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz", + "integrity": "sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==", + "license": "MIT", + "engines": { + "node": ">= 6.3.0" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/srcset": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", + "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, + "node_modules/svgo": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.4.tgz", + "integrity": "sha512-GsNRis4e8jxn2Y9ENz/8lbJ93CstG8svtMnuRaHbiF2LTJ5tK0/q3t/URPq9Zc7zVWBJnNnJMIp6bevK7bSmNg==", + "license": "MIT", + "dependencies": { + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/swc-loader": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.7.tgz", + "integrity": "sha512-nwYWw3Fh9ame3Rtm7StS9SBLpHRRnYcK7bnpF3UKZmesAK0gw2/ADvlURFAINmPvKtDLzp+GBiP9yLoEjg6S9w==", + "license": "MIT", + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "peerDependencies": { + "@swc/core": "^1.2.147", + "webpack": ">=2" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.49.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.2.tgz", + "integrity": "sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/terser-webpack-plugin/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/thingies": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.1.tgz", + "integrity": "sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==", + "license": "MIT", + "engines": { + "node": ">=10.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "license": "MIT" + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tree-dump": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", + "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-notifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", + "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^7.0.0", + "chalk": "^5.0.1", + "configstore": "^6.0.0", + "has-yarn": "^3.0.0", + "import-lazy": "^4.0.0", + "is-ci": "^3.0.1", + "is-installed-globally": "^0.4.0", + "is-npm": "^6.0.0", + "is-yarn-global": "^0.4.0", + "latest-version": "^7.0.0", + "pupa": "^3.1.0", + "semver": "^7.3.7", + "semver-diff": "^4.0.0", + "xdg-basedir": "^5.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/boxen": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", + "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", + "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "mime-types": "^2.1.27", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/url-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/url-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/url-loader/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/url-loader/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/url-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/value-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webpack": { + "version": "5.109.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.2.tgz", + "integrity": "sha512-U9/cvLzxObKNEZ9+TtdqrHM5/9z3lgl2c+c4BzbqGxFQvQvBAq87yql5A8pQ+rrMbS496MZJeF5enVBndIy2hw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.6.1", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz", + "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.43.1", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-middleware/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/webpack-dev-middleware/node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/webpack-dev-server": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.6.tgz", + "integrity": "sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw==", + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.25", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.8.1", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.22.1", + "graceful-fs": "^4.2.6", + "http-proxy-middleware": "^2.0.9", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.14.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^5.5.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.21.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz", + "integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpackbar": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-7.0.0.tgz", + "integrity": "sha512-aS9soqSO2iCHgqHoCrj4LbfGQUboDCYJPSFOAchEK+9psIjNrfSWW4Y0YEz67MKURNvMmfo0ycOg9d/+OOf9/Q==", + "license": "MIT", + "dependencies": { + "ansis": "^3.2.0", + "consola": "^3.2.3", + "pretty-time": "^1.1.0", + "std-env": "^3.7.0" + }, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@rspack/core": "*", + "webpack": "3 || 4 || 5" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wsl-utils/node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xdg-basedir": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", + "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 00000000..594555c6 --- /dev/null +++ b/website/package.json @@ -0,0 +1,46 @@ +{ + "name": "website", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids" + }, + "dependencies": { + "@docusaurus/core": "3.10.2", + "@docusaurus/faster": "3.10.2", + "@docusaurus/plugin-client-redirects": "^3.10.2", + "@docusaurus/preset-classic": "3.10.2", + "@mdx-js/react": "^3.0.0", + "clsx": "^2.0.0", + "prism-react-renderer": "^2.3.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "3.10.2", + "@docusaurus/types": "3.10.2" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome version", + "last 3 firefox version", + "last 5 safari version" + ] + }, + "engines": { + "node": ">=20.0" + } +} diff --git a/website/sidebars.js b/website/sidebars.js new file mode 100644 index 00000000..f32c6e81 --- /dev/null +++ b/website/sidebars.js @@ -0,0 +1,82 @@ +/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ +module.exports = { + docs: [ + 'intro', + 'getting-started/installation', + 'upgrading', + 'concepts/overview', + { + type: 'category', + label: 'API Reference', + collapsed: false, + items: [ + 'concepts/resources', + 'concepts/relationships', + 'concepts/persisting', + 'concepts/endpoints', + 'concepts/links', + ], + }, + { + type: 'category', + label: 'Topics', + items: [ + 'topics/testing', + 'topics/error-handling', + 'topics/authorization', + 'topics/debugging', + 'topics/caching', + 'topics/etags', + 'topics/json-attributes', + 'topics/customizing-sideloads', + 'concepts/backends-and-models', + 'topics/without-activerecord', + 'topics/openstruct-models', + 'topics/remote-resources', + 'topics/hopping-relationships', + ], + }, + { + type: 'category', + label: 'JavaScript Client', + items: [ + 'js/index', + 'js/installation', + 'js/models', + 'js/reads', + 'js/writes', + 'js/middleware', + 'js/authentication', + 'js/state-syncing', + 'js/ddau', + 'js/extra-params', + ], + }, + { + type: 'category', + label: 'Tutorial', + link: {type: 'doc', id: 'tutorial/index'}, + items: [ + 'getting-started/first-api', + 'tutorial/step_0', + 'tutorial/step_1', + 'tutorial/step_2', + 'tutorial/step_3', + 'tutorial/step_4', + 'tutorial/step_5', + 'tutorial/step_6', + 'tutorial/step_7', + 'tutorial/step_8', + 'tutorial/step_9', + ], + }, + { + type: 'category', + label: 'More', + items: [ + 'reference/vandal', + 'reference/why', + ], + }, + ], +}; diff --git a/website/src/css/custom.css b/website/src/css/custom.css new file mode 100644 index 00000000..b3337792 --- /dev/null +++ b/website/src/css/custom.css @@ -0,0 +1,85 @@ +/* Palette ported from the Jekyll site so this still reads as Graphiti. */ +@import url('https://fonts.googleapis.com/css2?family=Bungee&family=Roboto:wght@400;500;900&family=Roboto+Mono:wght@400;500&display=swap'); + +:root { + --ifm-color-primary: #e0289d; + --ifm-color-primary-dark: #ca248d; + --ifm-color-primary-darker: #bf2289; + --ifm-color-primary-darkest: #9d1c71; + --ifm-color-primary-light: #ff66d9; + --ifm-color-primary-lighter: #ff7cdf; + --ifm-color-primary-lightest: #ffa3e9; + + --ifm-font-family-base: 'Roboto', system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif; + --ifm-heading-font-family: 'Roboto', system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif; + --ifm-font-family-monospace: 'Roboto Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + + /* Roboto Black */ + --ifm-heading-font-weight: 900; + + --ifm-code-font-size: 95%; + --docusaurus-highlighted-code-line-bg: rgba(255, 102, 217, 0.12); +} + +html[data-theme='dark'] { + --ifm-color-primary: #ff66d9; + --ifm-color-primary-dark: #ff45d1; + --ifm-color-primary-darker: #ff34cd; + --ifm-color-primary-darkest: #f800bd; + --ifm-color-primary-light: #ff87e1; + --ifm-color-primary-lighter: #ff98e5; + --ifm-color-primary-lightest: #ffcbf1; + + --ifm-background-color: #292e33; + --ifm-navbar-background-color: #292e33; + --docusaurus-highlighted-code-line-bg: rgba(255, 102, 217, 0.2); +} + +html[data-theme='dark'] .footer--dark { + --ifm-footer-background-color: #21252a; +} + +.navbar__title { font-family: 'Bungee', cursive; font-weight: 400; font-size: 1.35rem; } + +/* Black sets tight at display sizes and needs more room above it than a + lighter face would. The smallest headings drop to Bold so a dense + reference page doesn't turn into a wall of Black. */ +.markdown h1 { letter-spacing: -0.015em; } +.markdown h2, +.markdown h3 { letter-spacing: -0.01em; margin-top: 2rem; } +.markdown h4, +.markdown h5, +.markdown h6 { font-weight: 700; } + +/* Infima aligns inline code to `middle`, which centres the padded box on the + parent's x-height rather than matching baselines. Roboto Mono's descenders + then push the glyphs visibly below the surrounding prose. Baseline puts the + letters back on the line and lets the padding grow around them. */ +.markdown code:not([class*='language-']) { + vertical-align: baseline; + padding: 0.15em 0.3em; +} + +/* The bar is full-bleed on every page, so it takes a tint of the brand pink + rather than the pink itself, which at that width reads as a promo banner. + Docusaurus hashes the class name, hence the prefix selector. */ +.theme-announcement-bar { + background: #fdeef7; + color: #7a1758; + font-size: 1.1rem; + border-bottom: 1px solid rgba(224, 40, 157, 0.25); + font-weight: 500; +} + +[data-theme='dark'] .theme-announcement-bar { + background: #33272f; + color: #ffcbf1; + border-bottom-color: rgba(255, 102, 217, 0.25); +} + +/* The link and the close button both carry their own colour, so they drift + from the bar's text unless told to inherit it. */ +.theme-announcement-bar a, +.theme-announcement-bar button { + color: inherit; +} diff --git a/website/src/pages/markdown-page.mdx b/website/src/pages/markdown-page.mdx new file mode 100644 index 00000000..9756c5b6 --- /dev/null +++ b/website/src/pages/markdown-page.mdx @@ -0,0 +1,7 @@ +--- +title: Markdown page example +--- + +# Markdown page example + +You don't need React to write simple standalone pages. diff --git a/website/static/.nojekyll b/website/static/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/website/static/1.13/2019/03/31/graphiti-1-0.html b/website/static/1.13/2019/03/31/graphiti-1-0.html new file mode 100644 index 00000000..b30374fa --- /dev/null +++ b/website/static/1.13/2019/03/31/graphiti-1-0.html @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti 1.0 Released 🎉 + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Graphiti 1.0 Released 🎉

+

This project has gone through a number of iterations over the past three years. I’m happy to say we’ve now released 1.0, committing to semantic versioning. Graphiti has now seen enough use, across a wide variety of scenarios, that we can commit to no backwards-incompatible changes for a long while, and a changelog for future releases.

+ +

We’ll be writing more in this blog about our future roadmap. Stay tuned!

+ +


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

+ +
+
+
+ + + + + + + diff --git a/website/static/1.13/2019/05/08/graphiti-1-1.html b/website/static/1.13/2019/05/08/graphiti-1-1.html new file mode 100644 index 00000000..babc3adf --- /dev/null +++ b/website/static/1.13/2019/05/08/graphiti-1-1.html @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti 1.1 Released + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Graphiti 1.1 Released

+

Our first minor version bump centers around an important update: +better errors for clients. This is the first phase of work to allow us to provide more actionable errors to API users who might not also be developers of the API itself. This was brought to us by Graphiti maintainer Wade Tandy.

+ +

Previously, when a client sent a bad write request to the server - +unwritable attributes/relationships, or bad types - we would render the +same generic 500 error JSON. We now render helpful error messages, so +third-party clients know how to correct their requests.

+ +

This is currently only affecting invalid writes, but the foundation laid +will be extendable to reads as well.

+ +


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

+ +
+
+
+ + + + + + + diff --git a/website/static/1.13/2019/05/20/graphiti-1-2.html b/website/static/1.13/2019/05/20/graphiti-1-2.html new file mode 100644 index 00000000..18caa478 --- /dev/null +++ b/website/static/1.13/2019/05/20/graphiti-1-2.html @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti 1.2 Released + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Graphiti 1.2 Released

+

I’m thrilled to announce much-needed, much-improved Rails integration, +courtesy of the amazing Peter Wagenet and +the new graphiti-rails gem. +This gives us better error handling, tighter controllers, and a solid +foundation for the codebase moving forward.

+ +

You’ll be especially happy about this release if you’ve ever tried to +add error-handling middleware. Because prior versions of Graphiti used +rescue_from in ApplicationController, we intercepted errors before +other middleware had access to them. Now everything just works.

+ +

As part of this upgrade, we’re switching from GraphitiErrors to +RescueRegistry. Think of +RescueRegistry as a well-refactored version of GraphitiErrors, available to +even non-Graphiti developers.

+ +

Though you don’t have to do anything to upgrade to Graphiti 1.2, this release will start throwing deprecation notices around ApplicationController. Don’t worry, the required changes are quite minor and spelled out in the graphiti-rails migration guide.

+ +

I’m quite happy with how this effort turned out. This release dramatically improves our integration with Rails internals, and I think everyone involved learned a lot. I’m honored to work with such talented developers in our open-source community ❤️

+ +

If you have any further questions, check out the #dev or #rails +channels in our Discord Chat

+ +


+
+
+
+
+
+
+
+
+
+

+ +
+
+
+ + + + + + + diff --git a/website/static/1.13/2019/10/14/tutorial.html b/website/static/1.13/2019/10/14/tutorial.html new file mode 100644 index 00000000..267e2ea8 --- /dev/null +++ b/website/static/1.13/2019/10/14/tutorial.html @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + Tutorial Write-Ups Are Out! + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Tutorial Write-Ups Are Out!

+

Though we’ve long had a sample application with step-by-step diffs, we’ve been missing a full walkthrough. That now exists! Start with Step 0: Bootstrapping.

+ +

These write-ups will tell you the relevant code and commands, but +they’ll also give helpful context around the decisions Graphiti makes. +Even if you’re already a Graphiti user, I suggest checking it out!

+ +


+
+
+
+
+
+
+
+
+
+

+ + +
+
+
+ + + + + + + diff --git a/website/static/1.13/CNAME b/website/static/1.13/CNAME new file mode 100644 index 00000000..cede9b42 --- /dev/null +++ b/website/static/1.13/CNAME @@ -0,0 +1 @@ +www.graphiti.dev \ No newline at end of file diff --git a/website/static/1.13/README.md b/website/static/1.13/README.md new file mode 100644 index 00000000..7f23e570 --- /dev/null +++ b/website/static/1.13/README.md @@ -0,0 +1,16 @@ +Graphiti Documentation +========== + +This is a static website generated by [jekyll](https://jekyllrb.com). To +contribute: + +```bash +$ git checkout gh-pages +$ bundle install --binstubs +$ bin/jekyll serve +``` + +Comment/Uncomment `github.url` in `_config.yml` + +Always make sure files are compiled before pushing. You can do this with +`jekyll serve` or `jekyll build` diff --git a/website/static/1.13/assets/css/syntax.css b/website/static/1.13/assets/css/syntax.css new file mode 100644 index 00000000..2774b764 --- /dev/null +++ b/website/static/1.13/assets/css/syntax.css @@ -0,0 +1,60 @@ +.highlight { background: #ffffff; } +.highlight .c { color: #999988; font-style: italic } /* Comment */ +.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ +.highlight .k { font-weight: bold } /* Keyword */ +.highlight .o { font-weight: bold } /* Operator */ +.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ +.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ +.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #aa0000 } /* Generic.Error */ +.highlight .gh { color: #999999 } /* Generic.Heading */ +.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ +.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #555555 } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #aaaaaa } /* Generic.Subheading */ +.highlight .gt { color: #aa0000 } /* Generic.Traceback */ +.highlight .kc { font-weight: bold } /* Keyword.Constant */ +.highlight .kd { font-weight: bold } /* Keyword.Declaration */ +.highlight .kp { font-weight: bold } /* Keyword.Pseudo */ +.highlight .kr { font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ +.highlight .m { color: #009999 } /* Literal.Number */ +.highlight .s { color: #d14 } /* Literal.String */ +.highlight .na { color: #008080 } /* Name.Attribute */ +.highlight .nb { color: #0086B3 } /* Name.Builtin */ +.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ +.highlight .no { color: #008080 } /* Name.Constant */ +.highlight .ni { color: #800080 } /* Name.Entity */ +.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ +.highlight .nn { color: #555555 } /* Name.Namespace */ +.highlight .nt { color: #000080 } /* Name.Tag */ +.highlight .nv { color: #008080 } /* Name.Variable */ +.highlight .ow { font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mf { color: #009999 } /* Literal.Number.Float */ +.highlight .mh { color: #009999 } /* Literal.Number.Hex */ +.highlight .mi { color: #009999 } /* Literal.Number.Integer */ +.highlight .mo { color: #009999 } /* Literal.Number.Oct */ +.highlight .sb { color: #d14 } /* Literal.String.Backtick */ +.highlight .sc { color: #d14 } /* Literal.String.Char */ +.highlight .sd { color: #d14 } /* Literal.String.Doc */ +.highlight .s2 { color: #d14 } /* Literal.String.Double */ +.highlight .se { color: #d14 } /* Literal.String.Escape */ +.highlight .sh { color: #d14 } /* Literal.String.Heredoc */ +.highlight .si { color: #d14 } /* Literal.String.Interpol */ +.highlight .sx { color: #d14 } /* Literal.String.Other */ +.highlight .sr { color: #009926 } /* Literal.String.Regex */ +.highlight .s1 { color: #d14 } /* Literal.String.Single */ +.highlight .ss { color: #990073 } /* Literal.String.Symbol */ +.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #008080 } /* Name.Variable.Class */ +.highlight .vg { color: #008080 } /* Name.Variable.Global */ +.highlight .vi { color: #008080 } /* Name.Variable.Instance */ +.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ diff --git a/website/static/1.13/assets/favicons/android-chrome-192x192.png b/website/static/1.13/assets/favicons/android-chrome-192x192.png new file mode 100644 index 00000000..419f5c87 Binary files /dev/null and b/website/static/1.13/assets/favicons/android-chrome-192x192.png differ diff --git a/website/static/1.13/assets/favicons/android-chrome-256x256.png b/website/static/1.13/assets/favicons/android-chrome-256x256.png new file mode 100644 index 00000000..c25f7e17 Binary files /dev/null and b/website/static/1.13/assets/favicons/android-chrome-256x256.png differ diff --git a/website/static/1.13/assets/favicons/apple-touch-icon.png b/website/static/1.13/assets/favicons/apple-touch-icon.png new file mode 100644 index 00000000..947d5623 Binary files /dev/null and b/website/static/1.13/assets/favicons/apple-touch-icon.png differ diff --git a/website/static/1.13/assets/favicons/browserconfig.xml b/website/static/1.13/assets/favicons/browserconfig.xml new file mode 100644 index 00000000..0f235d4a --- /dev/null +++ b/website/static/1.13/assets/favicons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #f86da7 + + + diff --git a/website/static/1.13/assets/favicons/favicon-16x16.png b/website/static/1.13/assets/favicons/favicon-16x16.png new file mode 100644 index 00000000..aa3bb57d Binary files /dev/null and b/website/static/1.13/assets/favicons/favicon-16x16.png differ diff --git a/website/static/1.13/assets/favicons/favicon-32x32.png b/website/static/1.13/assets/favicons/favicon-32x32.png new file mode 100644 index 00000000..cf2fb7cf Binary files /dev/null and b/website/static/1.13/assets/favicons/favicon-32x32.png differ diff --git a/website/static/1.13/assets/favicons/favicon.ico b/website/static/1.13/assets/favicons/favicon.ico new file mode 100644 index 00000000..c00f6af7 Binary files /dev/null and b/website/static/1.13/assets/favicons/favicon.ico differ diff --git a/website/static/1.13/assets/favicons/mstile-150x150.png b/website/static/1.13/assets/favicons/mstile-150x150.png new file mode 100644 index 00000000..8937b8cc Binary files /dev/null and b/website/static/1.13/assets/favicons/mstile-150x150.png differ diff --git a/website/static/1.13/assets/favicons/safari-pinned-tab.svg b/website/static/1.13/assets/favicons/safari-pinned-tab.svg new file mode 100644 index 00000000..2617848a --- /dev/null +++ b/website/static/1.13/assets/favicons/safari-pinned-tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/static/1.13/assets/favicons/site.webmanifest b/website/static/1.13/assets/favicons/site.webmanifest new file mode 100644 index 00000000..bb1544ed --- /dev/null +++ b/website/static/1.13/assets/favicons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Graphiti", + "short_name": "Graphiti", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-256x256.png", + "sizes": "256x256", + "type": "image/png" + } + ], + "theme_color": "#f86da7", + "background_color": "#f86da7", + "display": "standalone" +} diff --git a/website/static/1.13/assets/img/backend.gif b/website/static/1.13/assets/img/backend.gif new file mode 100644 index 00000000..8219d4da Binary files /dev/null and b/website/static/1.13/assets/img/backend.gif differ diff --git a/website/static/1.13/assets/img/conformity.png b/website/static/1.13/assets/img/conformity.png new file mode 100644 index 00000000..b471388c Binary files /dev/null and b/website/static/1.13/assets/img/conformity.png differ diff --git a/website/static/1.13/assets/img/error_payload.png b/website/static/1.13/assets/img/error_payload.png new file mode 100644 index 00000000..da986058 Binary files /dev/null and b/website/static/1.13/assets/img/error_payload.png differ diff --git a/website/static/1.13/assets/img/fancy-cushion.png b/website/static/1.13/assets/img/fancy-cushion.png new file mode 100644 index 00000000..0f3e8716 Binary files /dev/null and b/website/static/1.13/assets/img/fancy-cushion.png differ diff --git a/website/static/1.13/assets/img/gh.png b/website/static/1.13/assets/img/gh.png new file mode 100644 index 00000000..73db1f61 Binary files /dev/null and b/website/static/1.13/assets/img/gh.png differ diff --git a/website/static/1.13/assets/img/home-bg.jpg b/website/static/1.13/assets/img/home-bg.jpg new file mode 100644 index 00000000..22ccab23 Binary files /dev/null and b/website/static/1.13/assets/img/home-bg.jpg differ diff --git a/website/static/1.13/assets/img/lifecycle.gif b/website/static/1.13/assets/img/lifecycle.gif new file mode 100644 index 00000000..de463c26 Binary files /dev/null and b/website/static/1.13/assets/img/lifecycle.gif differ diff --git a/website/static/1.13/assets/img/logo-500.png b/website/static/1.13/assets/img/logo-500.png new file mode 100644 index 00000000..3451b415 Binary files /dev/null and b/website/static/1.13/assets/img/logo-500.png differ diff --git a/website/static/1.13/assets/img/logo.png b/website/static/1.13/assets/img/logo.png new file mode 100644 index 00000000..5ed5ca09 Binary files /dev/null and b/website/static/1.13/assets/img/logo.png differ diff --git a/website/static/1.13/assets/img/love-graffiti.jpg b/website/static/1.13/assets/img/love-graffiti.jpg new file mode 100644 index 00000000..38c77636 Binary files /dev/null and b/website/static/1.13/assets/img/love-graffiti.jpg differ diff --git a/website/static/1.13/assets/img/meta_total_count.png b/website/static/1.13/assets/img/meta_total_count.png new file mode 100644 index 00000000..1cfcaf7c Binary files /dev/null and b/website/static/1.13/assets/img/meta_total_count.png differ diff --git a/website/static/1.13/assets/img/persist.jpg b/website/static/1.13/assets/img/persist.jpg new file mode 100644 index 00000000..fb73154c Binary files /dev/null and b/website/static/1.13/assets/img/persist.jpg differ diff --git a/website/static/1.13/assets/img/resource.gif b/website/static/1.13/assets/img/resource.gif new file mode 100644 index 00000000..d32b0237 Binary files /dev/null and b/website/static/1.13/assets/img/resource.gif differ diff --git a/website/static/1.13/assets/img/rest-graffiti.jpg b/website/static/1.13/assets/img/rest-graffiti.jpg new file mode 100644 index 00000000..5674e0d1 Binary files /dev/null and b/website/static/1.13/assets/img/rest-graffiti.jpg differ diff --git a/website/static/1.13/assets/img/rest1.gif b/website/static/1.13/assets/img/rest1.gif new file mode 100644 index 00000000..ea70520c Binary files /dev/null and b/website/static/1.13/assets/img/rest1.gif differ diff --git a/website/static/1.13/assets/img/rest2.gif b/website/static/1.13/assets/img/rest2.gif new file mode 100644 index 00000000..2918ca8e Binary files /dev/null and b/website/static/1.13/assets/img/rest2.gif differ diff --git a/website/static/1.13/assets/img/rest3.gif b/website/static/1.13/assets/img/rest3.gif new file mode 100644 index 00000000..2630423d Binary files /dev/null and b/website/static/1.13/assets/img/rest3.gif differ diff --git a/website/static/1.13/assets/img/rethink-rest-graffiti.jpg b/website/static/1.13/assets/img/rethink-rest-graffiti.jpg new file mode 100644 index 00000000..28292e0e Binary files /dev/null and b/website/static/1.13/assets/img/rethink-rest-graffiti.jpg differ diff --git a/website/static/1.13/assets/img/sunrise.png b/website/static/1.13/assets/img/sunrise.png new file mode 100644 index 00000000..a0fa4793 Binary files /dev/null and b/website/static/1.13/assets/img/sunrise.png differ diff --git a/website/static/1.13/assets/img/why.png b/website/static/1.13/assets/img/why.png new file mode 100644 index 00000000..391f9811 Binary files /dev/null and b/website/static/1.13/assets/img/why.png differ diff --git a/website/static/1.13/assets/js/highlight.pack.js b/website/static/1.13/assets/js/highlight.pack.js new file mode 100644 index 00000000..fdba04ef --- /dev/null +++ b/website/static/1.13/assets/js/highlight.pack.js @@ -0,0 +1,2 @@ +/*! highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */ +!function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,"&").replace(//g,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){s+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var l=0,s="",f=[];e.length||r.length;){var g=i();if(s+=n(a.substring(l,g[0].offset)),l=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===l);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return s+n(a.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return l("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function l(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=l(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!y[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=null!=E.sL?d():h(),k=""}function v(e){L+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(L+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var R,E=i||N,x={},L="";for(R=E;R!==N;R=R.parent)R.cN&&(L=p(R.cN,"",!0)+L);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(L+=C);return{r:B,value:L,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(y);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?"
":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,l,s=i(e);a(s)||(I.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,l=n.textContent,r=s?f(s,l,!0):g(l),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),l)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,s,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=y[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function R(){return x(y)}function w(e){return e=(e||"").toLowerCase(),y[e]||y[L[e]]}var E=[],x=Object.keys,y={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b://,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[t],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[t],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("ruby",function(e){var b="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},c={cN:"doctag",b:"@[A-Za-z]+"},a={b:"#<",e:">"},s=[e.C("#","$",{c:[c]}),e.C("^\\=begin","^\\=end",{c:[c],r:10}),e.C("^__END__","\\n$")],n={cN:"subst",b:"#\\{",e:"}",k:r},t={cN:"string",c:[e.BE,n],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{b:/<<(-?)\w+$/,e:/^\s*\w+$/}]},i={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:r},d=[t,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(s)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:b}),i].concat(s)},{b:e.IR+"::"},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":(?!\\s)",c:[t,{b:b}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{cN:"params",b:/\|/,e:/\|/,k:r},{b:"("+e.RSR+"|unless)\\s*",k:"unless",c:[a,{cN:"regexp",c:[e.BE,n],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(s),r:0}].concat(s);n.c=d,i.c=d;var l="[>?]>",o="[\\w#]+\\(\\w+\\):\\d+:\\d+>",u="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",w=[{b:/^\s*=>/,starts:{e:"$",c:d}},{cN:"meta",b:"^("+l+"|"+o+"|"+u+")",starts:{e:"$",c:d}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:s.concat(w).concat(d)}});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},a={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/\b-?[a-z\._]+\b/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,s,a,t]}});hljs.registerLanguage("sql",function(e){var t=e.C("--","$");return{cI:!0,i:/[<>{}*#]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment",e:/;/,eW:!0,l:/[\w\.]+/,k:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t]},e.CBCM,t]}});hljs.registerLanguage("diff",function(e){return{aliases:["patch"],c:[{cN:"meta",r:10,v:[{b:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"comment",v:[{b:/Index: /,e:/$/},{b:/={3,}/,e:/$/},{b:/^\-{3}/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+{3}/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"addition",b:"^\\!",e:"$"}]}});hljs.registerLanguage("python",function(e){var r={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},b={cN:"meta",b:/^(>>>|\.\.\.) /},c={cN:"subst",b:/\{/,e:/\}/,k:r,i:/#/},a={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[b],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[b],r:10},{b:/(fr|rf|f)'''/,e:/'''/,c:[b,c]},{b:/(fr|rf|f)"""/,e:/"""/,c:[b,c]},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},{b:/(fr|rf|f)'/,e:/'/,c:[c]},{b:/(fr|rf|f)"/,e:/"/,c:[c]},e.ASM,e.QSM]},s={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},i={cN:"params",b:/\(/,e:/\)/,c:["self",b,s,a]};return c.c=[a,s,b],{aliases:["py","gyp"],k:r,i:/(<\/|->|\?)|=>/,c:[b,s,a,e.HCM,{v:[{cN:"function",bK:"def"},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,i,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}}); \ No newline at end of file diff --git a/website/static/1.13/assets/main.css b/website/static/1.13/assets/main.css new file mode 100644 index 00000000..e47dd450 --- /dev/null +++ b/website/static/1.13/assets/main.css @@ -0,0 +1,15518 @@ +@charset "UTF-8"; +/* this mixin makes possible to add transition to a specific vendor, example: */ +/* -webkit-transition: -webkit-transform 0.5s linear; */ +@import url(https://fonts.googleapis.com/css?family=Roboto:400,100,100italic,300italic,300,400italic,500,500italic,700,700italic,900,900italic); +@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700,900,200italic,300italic,400italic,600italic,700italic,900italic); +@import url(https://fonts.googleapis.com/css?family=Lato:100,300,400,700,900,100italic,300italic,400italic,700italic,900italic); +@import url(https://fonts.googleapis.com/css?family=Merriweather:400,300italic,300,400italic,700,700italic); +@import url(https://fonts.googleapis.com/css?family=Karla:400,700); +*:focus { + outline: 0; +} + +.button { + display: inline-block; + vertical-align: middle; + zoom: 1; + padding: 13px 32px; + font-weight: 400; + font-size: 17px; + color: #fff !important; + text-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0; + border: 0px; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.3) 0 1px 2px, inset rgba(255, 255, 255, 0.88) 0px 1px 3px -1px; + background-color: #4589E3; + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #5DA4FF), color-stop(100%, #417BFF)); + background: -webkit-linear-gradient(#5DA4FF, #417BFF); + background: -moz-linear-gradient(#5DA4FF, #417BFF); + background: -o-linear-gradient(#5DA4FF, #417BFF); + background: linear-gradient(#5DA4FF, #417BFF); + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.button:hover { + color: #fff; + text-decoration: none; +} +.button:active { + box-shadow: inset 0px 3px 3px rgba(0, 0, 0, 0.29); +} +.button.button-small { + padding: 10px 33px; + border: 1px solid #68A2EE; + font-size: 14px; + font-weight: 500; +} + +.button-outline { + display: inline-block; + vertical-align: middle; + zoom: 1; + color: #fff; + padding: 12px 31px; + border: 2px solid #fff; + border-radius: 4px; + font-size: 16px; + font-weight: 400; + background: rgba(0, 0, 0, 0.15); + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.button-outline:hover { + text-decoration: none; + color: #ddd; + border-color: #ddd; +} + +.button-clear { + border: 0; + outline: none; + padding: 1px; + display: inline-block; + text-decoration: none; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + -ms-border-radius: 5px; + -o-border-radius: 5px; + border-radius: 5px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075); + -ms-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075); + -o-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075); + background: #adb2bb; + background: -webkit-linear-gradient(#ccd0d6, #adb2bb); + background: -moz-linear-gradient(#ccd0d6, #adb2bb); + background: -ms-linear-gradient(#ccd0d6, #adb2bb); + background: -o-linear-gradient(#ccd0d6, #adb2bb); + background: linear-gradient(#ccd0d6, #adb2bb); +} +.button-clear span { + display: block; + font-size: 14px; + padding-left: 23px; + padding-right: 23px; + height: 36px; + line-height: 33px; + border-radius: 4px; + font-weight: bold; + color: #444; + text-shadow: 0 1px 0 white; + background: #eff1f4; + background: -webkit-linear-gradient(#f7f8fa, #eff1f4); + background: -moz-linear-gradient(#f7f8fa, #eff1f4); + background: -ms-linear-gradient(#f7f8fa, #eff1f4); + background: -o-linear-gradient(#f7f8fa, #eff1f4); + background: linear-gradient(#f7f8fa, #eff1f4); + -webkit-box-shadow: inset 0 1px 0 white; + -moz-box-shadow: inset 0 1px 0 white; + -ms-box-shadow: inset 0 1px 0 white; + -o-box-shadow: inset 0 1px 0 white; + box-shadow: inset 0 1px 0 white; +} +.button-clear:hover { + text-decoration: none; +} +.button-clear:active, .button-clear.active { + background: #b5bac2; + background: -webkit-linear-gradient(#b5bac2, #b5bac2); + background: -moz-linear-gradient(#b5bac2, #b5bac2); + background: -ms-linear-gradient(#b5bac2, #b5bac2); + background: -o-linear-gradient(#b5bac2, #b5bac2); + background: linear-gradient(#b5bac2, #b5bac2); +} +.button-clear:active span, .button-clear.active span { + color: #444; + background: #eff1f4; + background: -webkit-linear-gradient(#e6e8eb, #eff1f4); + background: -moz-linear-gradient(#e6e8eb, #eff1f4); + background: -ms-linear-gradient(#e6e8eb, #eff1f4); + background: -o-linear-gradient(#e6e8eb, #eff1f4); + background: linear-gradient(#e6e8eb, #eff1f4); + -webkit-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); + -ms-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); + -o-box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); +} + +/* form controls */ +input[type=text].form-control, +input[type=email].form-control, +input[type=password].form-control { + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); + height: 39px; +} +input[type=text].form-control::-webkit-input-placeholder, +input[type=email].form-control::-webkit-input-placeholder, +input[type=password].form-control::-webkit-input-placeholder { + color: #B1B1B1; + font-weight: 400; + font-size: 14px; +} +input[type=text].form-control:-moz-placeholder, +input[type=email].form-control:-moz-placeholder, +input[type=password].form-control:-moz-placeholder { + color: #B1B1B1; + font-weight: 400; + font-size: 14px; +} +input[type=text].form-control::-moz-placeholder, +input[type=email].form-control::-moz-placeholder, +input[type=password].form-control::-moz-placeholder { + color: #B1B1B1; + font-weight: 400; + font-size: 14px; +} +input[type=text].form-control:-ms-input-placeholder, +input[type=email].form-control:-ms-input-placeholder, +input[type=password].form-control:-ms-input-placeholder { + color: #B1B1B1; + font-weight: 400; + font-size: 14px; +} +input[type=text].form-control:focus, +input[type=email].form-control:focus, +input[type=password].form-control:focus { + border-color: #98D2FF; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px rgba(166, 205, 236, 0.6); +} + +textarea.form-control { + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.2); +} +textarea.form-control::-webkit-input-placeholder { + color: #B1B1B1; + font-weight: 400; + font-size: 14px; +} +textarea.form-control:-moz-placeholder { + color: #B1B1B1; + font-weight: 400; + font-size: 14px; +} +textarea.form-control::-moz-placeholder { + color: #B1B1B1; + font-weight: 400; + font-size: 14px; +} +textarea.form-control:-ms-input-placeholder { + color: #B1B1B1; + font-weight: 400; + font-size: 14px; +} +textarea.form-control:focus { + border-color: #98D2FF; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px rgba(166, 205, 236, 0.6); +} + +.btn-shadow { + border: 0; + font-weight: 600; + color: #fff; + display: inline-block; + box-shadow: rgba(23, 43, 99, 0.24) 0 7px 28px; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 13px; + padding: 12px 23px; + text-shadow: 1px 1px rgba(0, 0, 0, 0.09); + border-radius: 4px; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; + /* sizes */ + /* colors */ +} +.btn-shadow:hover, .btn-shadow:focus { + color: #fff; + text-decoration: none; +} +.btn-shadow:active { + box-shadow: rgba(23, 43, 99, 0.24) 0 7px 28px, inset 0 2px 3px rgba(0, 0, 0, 0.3); +} +.btn-shadow.btn-shadow-sm { + font-size: 11px; + padding: 7px 14px; +} +.btn-shadow.btn-shadow-lg { + font-size: 14px; + padding: 16px 31px; +} +.btn-shadow.btn-shadow-default { + background: #fff; + color: #444; + text-shadow: none; +} +.btn-shadow.btn-shadow-primary { + background: linear-gradient(#2083fe, #1d7bf0); +} +.btn-shadow.btn-shadow-dark { + background: #2C394D; +} +.btn-shadow.btn-shadow-info { + background: #42B1E2; +} +.btn-shadow.btn-shadow-warning { + background: #E2B477; +} +.btn-shadow.btn-shadow-success { + background: #0ad6bc; +} + +/* vendor */ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + margin: 0; +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} + +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} + +audio:not([controls]) { + display: none; + height: 0; +} + +[hidden], +template { + display: none; +} + +a { + background-color: transparent; +} + +a:active, +a:hover { + outline: 0; +} + +abbr[title] { + border-bottom: 1px dotted; +} + +b, +strong { + font-weight: bold; +} + +dfn { + font-style: italic; +} + +h1 { + margin: 0.67em 0; + font-size: 2em; +} + +mark { + color: #000; + background: #ff0; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + border: 0; +} + +svg:not(:root) { + overflow: hidden; +} + +figure { + margin: 1em 40px; +} + +hr { + height: 0; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +pre { + overflow: auto; +} + +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} + +button, +input, +optgroup, +select, +textarea { + margin: 0; + font: inherit; + color: inherit; +} + +button { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +button, +html input[type=button], +input[type=reset], +input[type=submit] { + -webkit-appearance: button; + cursor: pointer; +} + +button[disabled], +html input[disabled] { + cursor: default; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +input { + line-height: normal; +} + +input[type=checkbox], +input[type=radio] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + height: auto; +} + +input[type=search] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type=search]::-webkit-search-cancel-button, +input[type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} + +fieldset { + padding: 0.35em 0.625em 0.75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} + +legend { + padding: 0; + border: 0; +} + +textarea { + overflow: auto; +} + +optgroup { + font-weight: bold; +} + +table { + border-spacing: 0; + border-collapse: collapse; +} + +td, +th { + padding: 0; +} + +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + *:before, + *:after { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} +@font-face { + font-family: "Glyphicons Halflings"; + src: url("../../fonts/glyphicons-halflings-regular.eot"); + src: url("../../fonts/glyphicons-halflings-regular.eot?#iefix") format("embedded-opentype"), url("../../fonts/glyphicons-halflings-regular.woff2") format("woff2"), url("../../fonts/glyphicons-halflings-regular.woff") format("woff"), url("../../fonts/glyphicons-halflings-regular.ttf") format("truetype"), url("../../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg"); +} +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: "Glyphicons Halflings"; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.glyphicon-asterisk:before { + content: "*"; +} + +.glyphicon-plus:before { + content: "+"; +} + +.glyphicon-euro:before, +.glyphicon-eur:before { + content: "€"; +} + +.glyphicon-minus:before { + content: "−"; +} + +.glyphicon-cloud:before { + content: "☁"; +} + +.glyphicon-envelope:before { + content: "✉"; +} + +.glyphicon-pencil:before { + content: "✏"; +} + +.glyphicon-glass:before { + content: "\e001"; +} + +.glyphicon-music:before { + content: "\e002"; +} + +.glyphicon-search:before { + content: "\e003"; +} + +.glyphicon-heart:before { + content: "\e005"; +} + +.glyphicon-star:before { + content: "\e006"; +} + +.glyphicon-star-empty:before { + content: "\e007"; +} + +.glyphicon-user:before { + content: "\e008"; +} + +.glyphicon-film:before { + content: "\e009"; +} + +.glyphicon-th-large:before { + content: "\e010"; +} + +.glyphicon-th:before { + content: "\e011"; +} + +.glyphicon-th-list:before { + content: "\e012"; +} + +.glyphicon-ok:before { + content: "\e013"; +} + +.glyphicon-remove:before { + content: "\e014"; +} + +.glyphicon-zoom-in:before { + content: "\e015"; +} + +.glyphicon-zoom-out:before { + content: "\e016"; +} + +.glyphicon-off:before { + content: "\e017"; +} + +.glyphicon-signal:before { + content: "\e018"; +} + +.glyphicon-cog:before { + content: "\e019"; +} + +.glyphicon-trash:before { + content: "\e020"; +} + +.glyphicon-home:before { + content: "\e021"; +} + +.glyphicon-file:before { + content: "\e022"; +} + +.glyphicon-time:before { + content: "\e023"; +} + +.glyphicon-road:before { + content: "\e024"; +} + +.glyphicon-download-alt:before { + content: "\e025"; +} + +.glyphicon-download:before { + content: "\e026"; +} + +.glyphicon-upload:before { + content: "\e027"; +} + +.glyphicon-inbox:before { + content: "\e028"; +} + +.glyphicon-play-circle:before { + content: "\e029"; +} + +.glyphicon-repeat:before { + content: "\e030"; +} + +.glyphicon-refresh:before { + content: "\e031"; +} + +.glyphicon-list-alt:before { + content: "\e032"; +} + +.glyphicon-lock:before { + content: "\e033"; +} + +.glyphicon-flag:before { + content: "\e034"; +} + +.glyphicon-headphones:before { + content: "\e035"; +} + +.glyphicon-volume-off:before { + content: "\e036"; +} + +.glyphicon-volume-down:before { + content: "\e037"; +} + +.glyphicon-volume-up:before { + content: "\e038"; +} + +.glyphicon-qrcode:before { + content: "\e039"; +} + +.glyphicon-barcode:before { + content: "\e040"; +} + +.glyphicon-tag:before { + content: "\e041"; +} + +.glyphicon-tags:before { + content: "\e042"; +} + +.glyphicon-book:before { + content: "\e043"; +} + +.glyphicon-bookmark:before { + content: "\e044"; +} + +.glyphicon-print:before { + content: "\e045"; +} + +.glyphicon-camera:before { + content: "\e046"; +} + +.glyphicon-font:before { + content: "\e047"; +} + +.glyphicon-bold:before { + content: "\e048"; +} + +.glyphicon-italic:before { + content: "\e049"; +} + +.glyphicon-text-height:before { + content: "\e050"; +} + +.glyphicon-text-width:before { + content: "\e051"; +} + +.glyphicon-align-left:before { + content: "\e052"; +} + +.glyphicon-align-center:before { + content: "\e053"; +} + +.glyphicon-align-right:before { + content: "\e054"; +} + +.glyphicon-align-justify:before { + content: "\e055"; +} + +.glyphicon-list:before { + content: "\e056"; +} + +.glyphicon-indent-left:before { + content: "\e057"; +} + +.glyphicon-indent-right:before { + content: "\e058"; +} + +.glyphicon-facetime-video:before { + content: "\e059"; +} + +.glyphicon-picture:before { + content: "\e060"; +} + +.glyphicon-map-marker:before { + content: "\e062"; +} + +.glyphicon-adjust:before { + content: "\e063"; +} + +.glyphicon-tint:before { + content: "\e064"; +} + +.glyphicon-edit:before { + content: "\e065"; +} + +.glyphicon-share:before { + content: "\e066"; +} + +.glyphicon-check:before { + content: "\e067"; +} + +.glyphicon-move:before { + content: "\e068"; +} + +.glyphicon-step-backward:before { + content: "\e069"; +} + +.glyphicon-fast-backward:before { + content: "\e070"; +} + +.glyphicon-backward:before { + content: "\e071"; +} + +.glyphicon-play:before { + content: "\e072"; +} + +.glyphicon-pause:before { + content: "\e073"; +} + +.glyphicon-stop:before { + content: "\e074"; +} + +.glyphicon-forward:before { + content: "\e075"; +} + +.glyphicon-fast-forward:before { + content: "\e076"; +} + +.glyphicon-step-forward:before { + content: "\e077"; +} + +.glyphicon-eject:before { + content: "\e078"; +} + +.glyphicon-chevron-left:before { + content: "\e079"; +} + +.glyphicon-chevron-right:before { + content: "\e080"; +} + +.glyphicon-plus-sign:before { + content: "\e081"; +} + +.glyphicon-minus-sign:before { + content: "\e082"; +} + +.glyphicon-remove-sign:before { + content: "\e083"; +} + +.glyphicon-ok-sign:before { + content: "\e084"; +} + +.glyphicon-question-sign:before { + content: "\e085"; +} + +.glyphicon-info-sign:before { + content: "\e086"; +} + +.glyphicon-screenshot:before { + content: "\e087"; +} + +.glyphicon-remove-circle:before { + content: "\e088"; +} + +.glyphicon-ok-circle:before { + content: "\e089"; +} + +.glyphicon-ban-circle:before { + content: "\e090"; +} + +.glyphicon-arrow-left:before { + content: "\e091"; +} + +.glyphicon-arrow-right:before { + content: "\e092"; +} + +.glyphicon-arrow-up:before { + content: "\e093"; +} + +.glyphicon-arrow-down:before { + content: "\e094"; +} + +.glyphicon-share-alt:before { + content: "\e095"; +} + +.glyphicon-resize-full:before { + content: "\e096"; +} + +.glyphicon-resize-small:before { + content: "\e097"; +} + +.glyphicon-exclamation-sign:before { + content: "\e101"; +} + +.glyphicon-gift:before { + content: "\e102"; +} + +.glyphicon-leaf:before { + content: "\e103"; +} + +.glyphicon-fire:before { + content: "\e104"; +} + +.glyphicon-eye-open:before { + content: "\e105"; +} + +.glyphicon-eye-close:before { + content: "\e106"; +} + +.glyphicon-warning-sign:before { + content: "\e107"; +} + +.glyphicon-plane:before { + content: "\e108"; +} + +.glyphicon-calendar:before { + content: "\e109"; +} + +.glyphicon-random:before { + content: "\e110"; +} + +.glyphicon-comment:before { + content: "\e111"; +} + +.glyphicon-magnet:before { + content: "\e112"; +} + +.glyphicon-chevron-up:before { + content: "\e113"; +} + +.glyphicon-chevron-down:before { + content: "\e114"; +} + +.glyphicon-retweet:before { + content: "\e115"; +} + +.glyphicon-shopping-cart:before { + content: "\e116"; +} + +.glyphicon-folder-close:before { + content: "\e117"; +} + +.glyphicon-folder-open:before { + content: "\e118"; +} + +.glyphicon-resize-vertical:before { + content: "\e119"; +} + +.glyphicon-resize-horizontal:before { + content: "\e120"; +} + +.glyphicon-hdd:before { + content: "\e121"; +} + +.glyphicon-bullhorn:before { + content: "\e122"; +} + +.glyphicon-bell:before { + content: "\e123"; +} + +.glyphicon-certificate:before { + content: "\e124"; +} + +.glyphicon-thumbs-up:before { + content: "\e125"; +} + +.glyphicon-thumbs-down:before { + content: "\e126"; +} + +.glyphicon-hand-right:before { + content: "\e127"; +} + +.glyphicon-hand-left:before { + content: "\e128"; +} + +.glyphicon-hand-up:before { + content: "\e129"; +} + +.glyphicon-hand-down:before { + content: "\e130"; +} + +.glyphicon-circle-arrow-right:before { + content: "\e131"; +} + +.glyphicon-circle-arrow-left:before { + content: "\e132"; +} + +.glyphicon-circle-arrow-up:before { + content: "\e133"; +} + +.glyphicon-circle-arrow-down:before { + content: "\e134"; +} + +.glyphicon-globe:before { + content: "\e135"; +} + +.glyphicon-wrench:before { + content: "\e136"; +} + +.glyphicon-tasks:before { + content: "\e137"; +} + +.glyphicon-filter:before { + content: "\e138"; +} + +.glyphicon-briefcase:before { + content: "\e139"; +} + +.glyphicon-fullscreen:before { + content: "\e140"; +} + +.glyphicon-dashboard:before { + content: "\e141"; +} + +.glyphicon-paperclip:before { + content: "\e142"; +} + +.glyphicon-heart-empty:before { + content: "\e143"; +} + +.glyphicon-link:before { + content: "\e144"; +} + +.glyphicon-phone:before { + content: "\e145"; +} + +.glyphicon-pushpin:before { + content: "\e146"; +} + +.glyphicon-usd:before { + content: "\e148"; +} + +.glyphicon-gbp:before { + content: "\e149"; +} + +.glyphicon-sort:before { + content: "\e150"; +} + +.glyphicon-sort-by-alphabet:before { + content: "\e151"; +} + +.glyphicon-sort-by-alphabet-alt:before { + content: "\e152"; +} + +.glyphicon-sort-by-order:before { + content: "\e153"; +} + +.glyphicon-sort-by-order-alt:before { + content: "\e154"; +} + +.glyphicon-sort-by-attributes:before { + content: "\e155"; +} + +.glyphicon-sort-by-attributes-alt:before { + content: "\e156"; +} + +.glyphicon-unchecked:before { + content: "\e157"; +} + +.glyphicon-expand:before { + content: "\e158"; +} + +.glyphicon-collapse-down:before { + content: "\e159"; +} + +.glyphicon-collapse-up:before { + content: "\e160"; +} + +.glyphicon-log-in:before { + content: "\e161"; +} + +.glyphicon-flash:before { + content: "\e162"; +} + +.glyphicon-log-out:before { + content: "\e163"; +} + +.glyphicon-new-window:before { + content: "\e164"; +} + +.glyphicon-record:before { + content: "\e165"; +} + +.glyphicon-save:before { + content: "\e166"; +} + +.glyphicon-open:before { + content: "\e167"; +} + +.glyphicon-saved:before { + content: "\e168"; +} + +.glyphicon-import:before { + content: "\e169"; +} + +.glyphicon-export:before { + content: "\e170"; +} + +.glyphicon-send:before { + content: "\e171"; +} + +.glyphicon-floppy-disk:before { + content: "\e172"; +} + +.glyphicon-floppy-saved:before { + content: "\e173"; +} + +.glyphicon-floppy-remove:before { + content: "\e174"; +} + +.glyphicon-floppy-save:before { + content: "\e175"; +} + +.glyphicon-floppy-open:before { + content: "\e176"; +} + +.glyphicon-credit-card:before { + content: "\e177"; +} + +.glyphicon-transfer:before { + content: "\e178"; +} + +.glyphicon-cutlery:before { + content: "\e179"; +} + +.glyphicon-header:before { + content: "\e180"; +} + +.glyphicon-compressed:before { + content: "\e181"; +} + +.glyphicon-earphone:before { + content: "\e182"; +} + +.glyphicon-phone-alt:before { + content: "\e183"; +} + +.glyphicon-tower:before { + content: "\e184"; +} + +.glyphicon-stats:before { + content: "\e185"; +} + +.glyphicon-sd-video:before { + content: "\e186"; +} + +.glyphicon-hd-video:before { + content: "\e187"; +} + +.glyphicon-subtitles:before { + content: "\e188"; +} + +.glyphicon-sound-stereo:before { + content: "\e189"; +} + +.glyphicon-sound-dolby:before { + content: "\e190"; +} + +.glyphicon-sound-5-1:before { + content: "\e191"; +} + +.glyphicon-sound-6-1:before { + content: "\e192"; +} + +.glyphicon-sound-7-1:before { + content: "\e193"; +} + +.glyphicon-copyright-mark:before { + content: "\e194"; +} + +.glyphicon-registration-mark:before { + content: "\e195"; +} + +.glyphicon-cloud-download:before { + content: "\e197"; +} + +.glyphicon-cloud-upload:before { + content: "\e198"; +} + +.glyphicon-tree-conifer:before { + content: "\e199"; +} + +.glyphicon-tree-deciduous:before { + content: "\e200"; +} + +.glyphicon-cd:before { + content: "\e201"; +} + +.glyphicon-save-file:before { + content: "\e202"; +} + +.glyphicon-open-file:before { + content: "\e203"; +} + +.glyphicon-level-up:before { + content: "\e204"; +} + +.glyphicon-copy:before { + content: "\e205"; +} + +.glyphicon-paste:before { + content: "\e206"; +} + +.glyphicon-alert:before { + content: "\e209"; +} + +.glyphicon-equalizer:before { + content: "\e210"; +} + +.glyphicon-king:before { + content: "\e211"; +} + +.glyphicon-queen:before { + content: "\e212"; +} + +.glyphicon-pawn:before { + content: "\e213"; +} + +.glyphicon-bishop:before { + content: "\e214"; +} + +.glyphicon-knight:before { + content: "\e215"; +} + +.glyphicon-baby-formula:before { + content: "\e216"; +} + +.glyphicon-tent:before { + content: "⛺"; +} + +.glyphicon-blackboard:before { + content: "\e218"; +} + +.glyphicon-bed:before { + content: "\e219"; +} + +.glyphicon-apple:before { + content: "\f8ff"; +} + +.glyphicon-erase:before { + content: "\e221"; +} + +.glyphicon-hourglass:before { + content: "⌛"; +} + +.glyphicon-lamp:before { + content: "\e223"; +} + +.glyphicon-duplicate:before { + content: "\e224"; +} + +.glyphicon-piggy-bank:before { + content: "\e225"; +} + +.glyphicon-scissors:before { + content: "\e226"; +} + +.glyphicon-bitcoin:before { + content: "\e227"; +} + +.glyphicon-btc:before { + content: "\e227"; +} + +.glyphicon-xbt:before { + content: "\e227"; +} + +.glyphicon-yen:before { + content: "¥"; +} + +.glyphicon-jpy:before { + content: "¥"; +} + +.glyphicon-ruble:before { + content: "₽"; +} + +.glyphicon-rub:before { + content: "₽"; +} + +.glyphicon-scale:before { + content: "\e230"; +} + +.glyphicon-ice-lolly:before { + content: "\e231"; +} + +.glyphicon-ice-lolly-tasted:before { + content: "\e232"; +} + +.glyphicon-education:before { + content: "\e233"; +} + +.glyphicon-option-horizontal:before { + content: "\e234"; +} + +.glyphicon-option-vertical:before { + content: "\e235"; +} + +.glyphicon-menu-hamburger:before { + content: "\e236"; +} + +.glyphicon-modal-window:before { + content: "\e237"; +} + +.glyphicon-oil:before { + content: "\e238"; +} + +.glyphicon-grain:before { + content: "\e239"; +} + +.glyphicon-sunglasses:before { + content: "\e240"; +} + +.glyphicon-text-size:before { + content: "\e241"; +} + +.glyphicon-text-color:before { + content: "\e242"; +} + +.glyphicon-text-background:before { + content: "\e243"; +} + +.glyphicon-object-align-top:before { + content: "\e244"; +} + +.glyphicon-object-align-bottom:before { + content: "\e245"; +} + +.glyphicon-object-align-horizontal:before { + content: "\e246"; +} + +.glyphicon-object-align-left:before { + content: "\e247"; +} + +.glyphicon-object-align-vertical:before { + content: "\e248"; +} + +.glyphicon-object-align-right:before { + content: "\e249"; +} + +.glyphicon-triangle-right:before { + content: "\e250"; +} + +.glyphicon-triangle-left:before { + content: "\e251"; +} + +.glyphicon-triangle-bottom:before { + content: "\e252"; +} + +.glyphicon-triangle-top:before { + content: "\e253"; +} + +.glyphicon-console:before { + content: "\e254"; +} + +.glyphicon-superscript:before { + content: "\e255"; +} + +.glyphicon-subscript:before { + content: "\e256"; +} + +.glyphicon-menu-left:before { + content: "\e257"; +} + +.glyphicon-menu-right:before { + content: "\e258"; +} + +.glyphicon-menu-down:before { + content: "\e259"; +} + +.glyphicon-menu-up:before { + content: "\e260"; +} + +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +html { + font-size: 10px; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333; + background-color: #fff; +} + +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +a { + color: #337ab7; + text-decoration: none; +} + +a:hover, +a:focus { + color: #23527c; + text-decoration: underline; +} + +a:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +figure { + margin: 0; +} + +img { + vertical-align: middle; +} + +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + max-width: 100%; + height: auto; +} + +.img-rounded { + border-radius: 6px; +} + +.img-thumbnail { + display: inline-block; + max-width: 100%; + height: auto; + padding: 4px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +.img-circle { + border-radius: 50%; +} + +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eee; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} + +[role=button] { + cursor: pointer; +} + +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #777; +} + +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} + +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} + +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} + +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} + +h1, +.h1 { + font-size: 36px; +} + +h2, +.h2 { + font-size: 30px; +} + +h3, +.h3 { + font-size: 24px; +} + +h4, +.h4 { + font-size: 18px; +} + +h5, +.h5 { + font-size: 14px; +} + +h6, +.h6 { + font-size: 12px; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} + +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +small, +.small { + font-size: 85%; +} + +mark, +.mark { + padding: 0.2em; + background-color: #fcf8e3; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +.text-justify { + text-align: justify; +} + +.text-nowrap { + white-space: nowrap; +} + +.text-lowercase { + text-transform: lowercase; +} + +.text-uppercase { + text-transform: uppercase; +} + +.text-capitalize { + text-transform: capitalize; +} + +.text-muted { + color: #777; +} + +.text-primary { + color: #337ab7; +} + +a.text-primary:hover, +a.text-primary:focus { + color: #286090; +} + +.text-success { + color: #3c763d; +} + +a.text-success:hover, +a.text-success:focus { + color: #2b542c; +} + +.text-info { + color: #31708f; +} + +a.text-info:hover, +a.text-info:focus { + color: #245269; +} + +.text-warning { + color: #8a6d3b; +} + +a.text-warning:hover, +a.text-warning:focus { + color: #66512c; +} + +.text-danger { + color: #a94442; +} + +a.text-danger:hover, +a.text-danger:focus { + color: #843534; +} + +.bg-primary { + color: #fff; + background-color: #337ab7; +} + +a.bg-primary:hover, +a.bg-primary:focus { + background-color: #286090; +} + +.bg-success { + background-color: #dff0d8; +} + +a.bg-success:hover, +a.bg-success:focus { + background-color: #c1e2b3; +} + +.bg-info { + background-color: #d9edf7; +} + +a.bg-info:hover, +a.bg-info:focus { + background-color: #afd9ee; +} + +.bg-warning { + background-color: #fcf8e3; +} + +a.bg-warning:hover, +a.bg-warning:focus { + background-color: #f7ecb5; +} + +.bg-danger { + background-color: #f2dede; +} + +a.bg-danger:hover, +a.bg-danger:focus { + background-color: #e4b9b9; +} + +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eee; +} + +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} + +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + margin-left: -5px; + list-style: none; +} + +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} + +dl { + margin-top: 0; + margin-bottom: 20px; +} + +dt, +dd { + line-height: 1.42857143; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 0; +} + +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #777; +} + +.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eee; +} + +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} + +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777; +} + +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: "— "; +} + +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + text-align: right; + border-right: 5px solid #eee; + border-left: 0; +} + +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ""; +} + +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: " —"; +} + +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} + +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} + +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} + +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} + +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +.row { + margin-right: -15px; + margin-left: -15px; +} + +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} + +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} + +.col-xs-12 { + width: 100%; +} + +.col-xs-11 { + width: 91.66666667%; +} + +.col-xs-10 { + width: 83.33333333%; +} + +.col-xs-9 { + width: 75%; +} + +.col-xs-8 { + width: 66.66666667%; +} + +.col-xs-7 { + width: 58.33333333%; +} + +.col-xs-6 { + width: 50%; +} + +.col-xs-5 { + width: 41.66666667%; +} + +.col-xs-4 { + width: 33.33333333%; +} + +.col-xs-3 { + width: 25%; +} + +.col-xs-2 { + width: 16.66666667%; +} + +.col-xs-1 { + width: 8.33333333%; +} + +.col-xs-pull-12 { + right: 100%; +} + +.col-xs-pull-11 { + right: 91.66666667%; +} + +.col-xs-pull-10 { + right: 83.33333333%; +} + +.col-xs-pull-9 { + right: 75%; +} + +.col-xs-pull-8 { + right: 66.66666667%; +} + +.col-xs-pull-7 { + right: 58.33333333%; +} + +.col-xs-pull-6 { + right: 50%; +} + +.col-xs-pull-5 { + right: 41.66666667%; +} + +.col-xs-pull-4 { + right: 33.33333333%; +} + +.col-xs-pull-3 { + right: 25%; +} + +.col-xs-pull-2 { + right: 16.66666667%; +} + +.col-xs-pull-1 { + right: 8.33333333%; +} + +.col-xs-pull-0 { + right: auto; +} + +.col-xs-push-12 { + left: 100%; +} + +.col-xs-push-11 { + left: 91.66666667%; +} + +.col-xs-push-10 { + left: 83.33333333%; +} + +.col-xs-push-9 { + left: 75%; +} + +.col-xs-push-8 { + left: 66.66666667%; +} + +.col-xs-push-7 { + left: 58.33333333%; +} + +.col-xs-push-6 { + left: 50%; +} + +.col-xs-push-5 { + left: 41.66666667%; +} + +.col-xs-push-4 { + left: 33.33333333%; +} + +.col-xs-push-3 { + left: 25%; +} + +.col-xs-push-2 { + left: 16.66666667%; +} + +.col-xs-push-1 { + left: 8.33333333%; +} + +.col-xs-push-0 { + left: auto; +} + +.col-xs-offset-12 { + margin-left: 100%; +} + +.col-xs-offset-11 { + margin-left: 91.66666667%; +} + +.col-xs-offset-10 { + margin-left: 83.33333333%; +} + +.col-xs-offset-9 { + margin-left: 75%; +} + +.col-xs-offset-8 { + margin-left: 66.66666667%; +} + +.col-xs-offset-7 { + margin-left: 58.33333333%; +} + +.col-xs-offset-6 { + margin-left: 50%; +} + +.col-xs-offset-5 { + margin-left: 41.66666667%; +} + +.col-xs-offset-4 { + margin-left: 33.33333333%; +} + +.col-xs-offset-3 { + margin-left: 25%; +} + +.col-xs-offset-2 { + margin-left: 16.66666667%; +} + +.col-xs-offset-1 { + margin-left: 8.33333333%; +} + +.col-xs-offset-0 { + margin-left: 0; +} + +@media (min-width: 768px) { + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0; + } +} +@media (min-width: 992px) { + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0; + } +} +@media (min-width: 1200px) { + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0; + } +} +table { + background-color: transparent; +} + +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #777; + text-align: left; +} + +th { + text-align: left; +} + +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} + +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; +} + +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; +} + +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} + +.table > tbody + tbody { + border-top: 2px solid #ddd; +} + +.table .table { + background-color: #fff; +} + +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} + +.table-bordered { + border: 1px solid #ddd; +} + +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; +} + +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} + +.table-striped > tbody > tr:nth-of-type(odd) { + background-color: #f9f9f9; +} + +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} + +table col[class*=col-] { + position: static; + display: table-column; + float: none; +} + +table td[class*=col-], +table th[class*=col-] { + position: static; + display: table-cell; + float: none; +} + +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} + +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; +} + +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; +} + +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; +} + +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #d9edf7; +} + +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; +} + +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; +} + +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; +} + +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; +} + +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; +} + +.table-responsive { + min-height: 0.01%; + overflow-x: auto; +} + +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #ddd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: bold; +} + +input[type=search] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +input[type=radio], +input[type=checkbox] { + margin: 4px 0 0; + margin-top: 1px \9 ; + line-height: normal; +} + +input[type=file] { + display: block; +} + +input[type=range] { + display: block; + width: 100%; +} + +select[multiple], +select[size] { + height: auto; +} + +input[type=file]:focus, +input[type=radio]:focus, +input[type=checkbox]:focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857143; + color: #555; +} + +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border-color ease-in-out 0.15s, -webkit-box-shadow ease-in-out 0.15s; + -o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; +} + +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); +} + +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} + +.form-control:-ms-input-placeholder { + color: #999; +} + +.form-control::-webkit-input-placeholder { + color: #999; +} + +.form-control::-ms-expand { + background-color: transparent; + border: 0; +} + +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + background-color: #eee; + opacity: 1; +} + +.form-control[disabled], +fieldset[disabled] .form-control { + cursor: not-allowed; +} + +textarea.form-control { + height: auto; +} + +input[type=search] { + -webkit-appearance: none; +} + +@media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type=date].form-control, + input[type=time].form-control, + input[type=datetime-local].form-control, + input[type=month].form-control { + line-height: 34px; + } + input[type=date].input-sm, + input[type=time].input-sm, + input[type=datetime-local].input-sm, + input[type=month].input-sm, + .input-group-sm input[type=date], + .input-group-sm input[type=time], + .input-group-sm input[type=datetime-local], + .input-group-sm input[type=month] { + line-height: 30px; + } + input[type=date].input-lg, + input[type=time].input-lg, + input[type=datetime-local].input-lg, + input[type=month].input-lg, + .input-group-lg input[type=date], + .input-group-lg input[type=time], + .input-group-lg input[type=datetime-local], + .input-group-lg input[type=month] { + line-height: 46px; + } +} +.form-group { + margin-bottom: 15px; +} + +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} + +.radio label, +.checkbox label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; +} + +.radio input[type=radio], +.radio-inline input[type=radio], +.checkbox input[type=checkbox], +.checkbox-inline input[type=checkbox] { + position: absolute; + margin-top: 4px \9 ; + margin-left: -20px; +} + +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} + +.radio-inline, +.checkbox-inline { + position: relative; + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + vertical-align: middle; + cursor: pointer; +} + +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} + +input[type=radio][disabled], +input[type=checkbox][disabled], +input[type=radio].disabled, +input[type=checkbox].disabled, +fieldset[disabled] input[type=radio], +fieldset[disabled] input[type=checkbox] { + cursor: not-allowed; +} + +.radio-inline.disabled, +.checkbox-inline.disabled, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} + +.radio.disabled label, +.checkbox.disabled label, +fieldset[disabled] .radio label, +fieldset[disabled] .checkbox label { + cursor: not-allowed; +} + +.form-control-static { + min-height: 34px; + padding-top: 7px; + padding-bottom: 7px; + margin-bottom: 0; +} + +.form-control-static.input-lg, +.form-control-static.input-sm { + padding-right: 0; + padding-left: 0; +} + +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +select.input-sm { + height: 30px; + line-height: 30px; +} + +textarea.input-sm, +select[multiple].input-sm { + height: auto; +} + +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.form-group-sm select.form-control { + height: 30px; + line-height: 30px; +} + +.form-group-sm textarea.form-control, +.form-group-sm select[multiple].form-control { + height: auto; +} + +.form-group-sm .form-control-static { + height: 30px; + min-height: 32px; + padding: 6px 10px; + font-size: 12px; + line-height: 1.5; +} + +.input-lg { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} + +select.input-lg { + height: 46px; + line-height: 46px; +} + +textarea.input-lg, +select[multiple].input-lg { + height: auto; +} + +.form-group-lg .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} + +.form-group-lg select.form-control { + height: 46px; + line-height: 46px; +} + +.form-group-lg textarea.form-control, +.form-group-lg select[multiple].form-control { + height: auto; +} + +.form-group-lg .form-control-static { + height: 46px; + min-height: 38px; + padding: 11px 16px; + font-size: 18px; + line-height: 1.3333333; +} + +.has-feedback { + position: relative; +} + +.has-feedback .form-control { + padding-right: 42.5px; +} + +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + pointer-events: none; +} + +.input-lg + .form-control-feedback, +.input-group-lg + .form-control-feedback, +.form-group-lg .form-control + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; +} + +.input-sm + .form-control-feedback, +.input-group-sm + .form-control-feedback, +.form-group-sm .form-control + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} + +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #3c763d; +} + +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; +} + +.has-success .input-group-addon { + color: #3c763d; + background-color: #dff0d8; + border-color: #3c763d; +} + +.has-success .form-control-feedback { + color: #3c763d; +} + +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #8a6d3b; +} + +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b; +} + +.has-warning .input-group-addon { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #8a6d3b; +} + +.has-warning .form-control-feedback { + color: #8a6d3b; +} + +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #a94442; +} + +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; +} + +.has-error .input-group-addon { + color: #a94442; + background-color: #f2dede; + border-color: #a94442; +} + +.has-error .form-control-feedback { + color: #a94442; +} + +.has-feedback label ~ .form-control-feedback { + top: 25px; +} + +.has-feedback label.sr-only ~ .form-control-feedback { + top: 0; +} + +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} + +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type=radio], + .form-inline .checkbox input[type=checkbox] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} + +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 27px; +} + +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} + +@media (min-width: 768px) { + .form-horizontal .control-label { + padding-top: 7px; + margin-bottom: 0; + text-align: right; + } +} +.form-horizontal .has-feedback .form-control-feedback { + right: 15px; +} + +@media (min-width: 768px) { + .form-horizontal .form-group-lg .control-label { + padding-top: 11px; + font-size: 18px; + } +} +@media (min-width: 768px) { + .form-horizontal .form-group-sm .control-label { + padding-top: 6px; + font-size: 12px; + } +} +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} + +.btn:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn:hover, +.btn:focus, +.btn.focus { + color: #333; + text-decoration: none; +} + +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + cursor: not-allowed; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; + opacity: 0.65; +} + +a.btn.disabled, +fieldset[disabled] a.btn { + pointer-events: none; +} + +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} + +.btn-default:focus, +.btn-default.focus { + color: #333; + background-color: #e6e6e6; + border-color: #8c8c8c; +} + +.btn-default:hover { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} + +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} + +.btn-default:active:hover, +.btn-default.active:hover, +.open > .dropdown-toggle.btn-default:hover, +.btn-default:active:focus, +.btn-default.active:focus, +.open > .dropdown-toggle.btn-default:focus, +.btn-default:active.focus, +.btn-default.active.focus, +.open > .dropdown-toggle.btn-default.focus { + color: #333; + background-color: #d4d4d4; + border-color: #8c8c8c; +} + +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} + +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus { + background-color: #fff; + border-color: #ccc; +} + +.btn-default .badge { + color: #fff; + background-color: #333; +} + +.btn-primary { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; +} + +.btn-primary:focus, +.btn-primary.focus { + color: #fff; + background-color: #286090; + border-color: #122b40; +} + +.btn-primary:hover { + color: #fff; + background-color: #286090; + border-color: #204d74; +} + +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #286090; + border-color: #204d74; +} + +.btn-primary:active:hover, +.btn-primary.active:hover, +.open > .dropdown-toggle.btn-primary:hover, +.btn-primary:active:focus, +.btn-primary.active:focus, +.open > .dropdown-toggle.btn-primary:focus, +.btn-primary:active.focus, +.btn-primary.active.focus, +.open > .dropdown-toggle.btn-primary.focus { + color: #fff; + background-color: #204d74; + border-color: #122b40; +} + +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} + +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus { + background-color: #337ab7; + border-color: #2e6da4; +} + +.btn-primary .badge { + color: #337ab7; + background-color: #fff; +} + +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-success:focus, +.btn-success.focus { + color: #fff; + background-color: #449d44; + border-color: #255625; +} + +.btn-success:hover { + color: #fff; + background-color: #449d44; + border-color: #398439; +} + +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #449d44; + border-color: #398439; +} + +.btn-success:active:hover, +.btn-success.active:hover, +.open > .dropdown-toggle.btn-success:hover, +.btn-success:active:focus, +.btn-success.active:focus, +.open > .dropdown-toggle.btn-success:focus, +.btn-success:active.focus, +.btn-success.active.focus, +.open > .dropdown-toggle.btn-success.focus { + color: #fff; + background-color: #398439; + border-color: #255625; +} + +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} + +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus { + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-success .badge { + color: #5cb85c; + background-color: #fff; +} + +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-info:focus, +.btn-info.focus { + color: #fff; + background-color: #31b0d5; + border-color: #1b6d85; +} + +.btn-info:hover { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} + +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} + +.btn-info:active:hover, +.btn-info.active:hover, +.open > .dropdown-toggle.btn-info:hover, +.btn-info:active:focus, +.btn-info.active:focus, +.open > .dropdown-toggle.btn-info:focus, +.btn-info:active.focus, +.btn-info.active.focus, +.open > .dropdown-toggle.btn-info.focus { + color: #fff; + background-color: #269abc; + border-color: #1b6d85; +} + +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} + +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus { + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-info .badge { + color: #5bc0de; + background-color: #fff; +} + +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #eea236; +} + +.btn-warning:focus, +.btn-warning.focus { + color: #fff; + background-color: #ec971f; + border-color: #985f0d; +} + +.btn-warning:hover { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} + +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} + +.btn-warning:active:hover, +.btn-warning.active:hover, +.open > .dropdown-toggle.btn-warning:hover, +.btn-warning:active:focus, +.btn-warning.active:focus, +.open > .dropdown-toggle.btn-warning:focus, +.btn-warning:active.focus, +.btn-warning.active.focus, +.open > .dropdown-toggle.btn-warning.focus { + color: #fff; + background-color: #d58512; + border-color: #985f0d; +} + +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} + +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus { + background-color: #f0ad4e; + border-color: #eea236; +} + +.btn-warning .badge { + color: #f0ad4e; + background-color: #fff; +} + +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-danger:focus, +.btn-danger.focus { + color: #fff; + background-color: #c9302c; + border-color: #761c19; +} + +.btn-danger:hover { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} + +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} + +.btn-danger:active:hover, +.btn-danger.active:hover, +.open > .dropdown-toggle.btn-danger:hover, +.btn-danger:active:focus, +.btn-danger.active:focus, +.open > .dropdown-toggle.btn-danger:focus, +.btn-danger:active.focus, +.btn-danger.active.focus, +.open > .dropdown-toggle.btn-danger.focus { + color: #fff; + background-color: #ac2925; + border-color: #761c19; +} + +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} + +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus { + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-danger .badge { + color: #d9534f; + background-color: #fff; +} + +.btn-link { + font-weight: normal; + color: #337ab7; + border-radius: 0; +} + +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} + +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} + +.btn-link:hover, +.btn-link:focus { + color: #23527c; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #777; + text-decoration: none; +} + +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} + +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-block { + display: block; + width: 100%; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type=submit].btn-block, +input[type=reset].btn-block, +input[type=button].btn-block { + width: 100%; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + display: none; +} + +.collapse.in { + display: block; +} + +tr.collapse.in { + display: table-row; +} + +tbody.collapse.in { + display: table-row-group; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-timing-function: ease; + -o-transition-timing-function: ease; + transition-timing-function: ease; + -webkit-transition-duration: 0.35s; + -o-transition-duration: 0.35s; + transition-duration: 0.35s; + -webkit-transition-property: height, visibility; + -o-transition-property: height, visibility; + transition-property: height, visibility; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px dashed; + border-top: 4px solid \9 ; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} + +.dropup, +.dropdown { + position: relative; +} + +.dropdown-toggle:focus { + outline: 0; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333; + white-space: nowrap; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} + +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #fff; + text-decoration: none; + background-color: #337ab7; + outline: 0; +} + +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #777; +} + +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} + +.open > .dropdown-menu { + display: block; +} + +.open > a { + outline: 0; +} + +.dropdown-menu-right { + right: 0; + left: auto; +} + +.dropdown-menu-left { + right: auto; + left: 0; +} + +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: #777; + white-space: nowrap; +} + +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + content: ""; + border-top: 0; + border-bottom: 4px dashed; + border-bottom: 4px solid \9 ; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} + +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } + .navbar-right .dropdown-menu-left { + right: auto; + left: 0; + } +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} + +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} + +.btn-toolbar { + margin-left: -5px; +} + +.btn-toolbar .btn, +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} + +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +.btn-group > .btn:first-child { + margin-left: 0; +} + +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group > .btn-group { + float: left; +} + +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} + +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} + +.btn .caret { + margin-left: 0; +} + +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} + +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} + +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} + +.btn-group-vertical > .btn-group > .btn { + float: none; +} + +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} + +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} + +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + display: table-cell; + float: none; + width: 1%; +} + +.btn-group-justified > .btn-group .btn { + width: 100%; +} + +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} + +[data-toggle=buttons] > .btn input[type=radio], +[data-toggle=buttons] > .btn-group > .btn input[type=radio], +[data-toggle=buttons] > .btn input[type=checkbox], +[data-toggle=buttons] > .btn-group > .btn input[type=checkbox] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} + +.input-group { + position: relative; + display: table; + border-collapse: separate; +} + +.input-group[class*=col-] { + float: none; + padding-right: 0; + padding-left: 0; +} + +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} + +.input-group .form-control:focus { + z-index: 3; +} + +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; + border-radius: 6px; +} + +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 46px; + line-height: 46px; +} + +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn { + height: auto; +} + +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} + +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { + height: auto; +} + +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} + +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} + +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555; + text-align: center; + background-color: #eee; + border: 1px solid #ccc; + border-radius: 4px; +} + +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} + +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} + +.input-group-addon input[type=radio], +.input-group-addon input[type=checkbox] { + margin-top: 0; +} + +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group-addon:first-child { + border-right: 0; +} + +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group-addon:last-child { + border-left: 0; +} + +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} + +.input-group-btn > .btn { + position: relative; +} + +.input-group-btn > .btn + .btn { + margin-left: -1px; +} + +.input-group-btn > .btn:hover, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active { + z-index: 2; +} + +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} + +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + z-index: 2; + margin-left: -1px; +} + +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav > li { + position: relative; + display: block; +} + +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} + +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eee; +} + +.nav > li.disabled > a { + color: #777; +} + +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #777; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} + +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eee; + border-color: #337ab7; +} + +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.nav > li > a > img { + max-width: none; +} + +.nav-tabs { + border-bottom: 1px solid #ddd; +} + +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} + +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover { + border-color: #eee #eee #ddd; +} + +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555; + cursor: default; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} + +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} + +.nav-tabs.nav-justified > li { + float: none; +} + +.nav-tabs.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} + +.nav-tabs.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} + +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-tabs.nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 4px; +} + +.nav-tabs.nav-justified > .active > a, +.nav-tabs.nav-justified > .active > a:hover, +.nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #ddd; +} + +@media (min-width: 768px) { + .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs.nav-justified > .active > a, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.nav-pills > li { + float: left; +} + +.nav-pills > li > a { + border-radius: 4px; +} + +.nav-pills > li + li { + margin-left: 2px; +} + +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #fff; + background-color: #337ab7; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} + +.nav-justified { + width: 100%; +} + +.nav-justified > li { + float: none; +} + +.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} + +.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} + +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs-justified { + border-bottom: 0; +} + +.nav-tabs-justified > li > a { + margin-right: 0; + border-radius: 4px; +} + +.nav-tabs-justified > .active > a, +.nav-tabs-justified > .active > a:hover, +.nav-tabs-justified > .active > a:focus { + border: 1px solid #ddd; +} + +@media (min-width: 768px) { + .nav-tabs-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.tab-content > .tab-pane { + display: none; +} + +.tab-content > .active { + display: block; +} + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} + +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} +.navbar-collapse { + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + -webkit-overflow-scrolling: touch; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.navbar-collapse.in { + overflow-y: auto; +} + +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-right: 0; + padding-left: 0; + } +} +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} + +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} +.container > .navbar-header, +.container-fluid > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} + +@media (min-width: 768px) { + .container > .navbar-header, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; +} + +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} + +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} + +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; +} + +.navbar-brand { + float: left; + height: 50px; + padding: 15px 15px; + font-size: 18px; + line-height: 20px; +} + +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} + +.navbar-brand > img { + display: block; +} + +@media (min-width: 768px) { + .navbar > .container .navbar-brand, + .navbar > .container-fluid .navbar-brand { + margin-left: -15px; + } +} +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-top: 8px; + margin-right: 15px; + margin-bottom: 8px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} + +.navbar-toggle:focus { + outline: 0; +} + +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} + +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} + +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} +.navbar-nav { + margin: 7.5px -15px; +} + +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} + +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + } +} +.navbar-form { + padding: 10px 15px; + margin-top: 8px; + margin-right: -15px; + margin-bottom: 8px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); +} + +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .form-control-static { + display: inline-block; + } + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { + width: auto; + } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + .navbar-form .radio input[type=radio], + .navbar-form .checkbox input[type=checkbox] { + position: relative; + margin-left: 0; + } + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } +} +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + .navbar-form .form-group:last-child { + margin-bottom: 0; + } +} +@media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + margin-bottom: 0; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; +} + +.navbar-btn.btn-sm { + margin-top: 10px; + margin-bottom: 10px; +} + +.navbar-btn.btn-xs { + margin-top: 14px; + margin-bottom: 14px; +} + +.navbar-text { + margin-top: 15px; + margin-bottom: 15px; +} + +@media (min-width: 768px) { + .navbar-text { + float: left; + margin-right: 15px; + margin-left: 15px; + } +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: -15px; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} + +.navbar-default .navbar-brand { + color: #777; +} + +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #5e5e5e; + background-color: transparent; +} + +.navbar-default .navbar-text { + color: #777; +} + +.navbar-default .navbar-nav > li > a { + color: #777; +} + +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333; + background-color: transparent; +} + +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555; + background-color: #e7e7e7; +} + +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #ccc; + background-color: transparent; +} + +.navbar-default .navbar-toggle { + border-color: #ddd; +} + +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #ddd; +} + +.navbar-default .navbar-toggle .icon-bar { + background-color: #888; +} + +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e7e7e7; +} + +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + color: #555; + background-color: #e7e7e7; +} + +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #777; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #ccc; + background-color: transparent; + } +} +.navbar-default .navbar-link { + color: #777; +} + +.navbar-default .navbar-link:hover { + color: #333; +} + +.navbar-default .btn-link { + color: #777; +} + +.navbar-default .btn-link:hover, +.navbar-default .btn-link:focus { + color: #333; +} + +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #ccc; +} + +.navbar-inverse { + background-color: #222; + border-color: #080808; +} + +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} + +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #fff; + background-color: transparent; +} + +.navbar-inverse .navbar-text { + color: #9d9d9d; +} + +.navbar-inverse .navbar-nav > li > a { + color: #9d9d9d; +} + +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #fff; + background-color: transparent; +} + +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #fff; + background-color: #080808; +} + +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444; + background-color: transparent; +} + +.navbar-inverse .navbar-toggle { + border-color: #333; +} + +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333; +} + +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} + +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} + +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + color: #fff; + background-color: #080808; +} + +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #9d9d9d; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444; + background-color: transparent; + } +} +.navbar-inverse .navbar-link { + color: #9d9d9d; +} + +.navbar-inverse .navbar-link:hover { + color: #fff; +} + +.navbar-inverse .btn-link { + color: #9d9d9d; +} + +.navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link:focus { + color: #fff; +} + +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:focus { + color: #444; +} + +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} + +.breadcrumb > li { + display: inline-block; +} + +.breadcrumb > li + li:before { + padding: 0 5px; + color: #ccc; + content: "/ "; +} + +.breadcrumb > .active { + color: #777; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} + +.pagination > li { + display: inline; +} + +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.42857143; + color: #337ab7; + text-decoration: none; + background-color: #fff; + border: 1px solid #ddd; +} + +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + z-index: 2; + color: #23527c; + background-color: #eee; + border-color: #ddd; +} + +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 3; + color: #fff; + cursor: default; + background-color: #337ab7; + border-color: #337ab7; +} + +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #777; + cursor: not-allowed; + background-color: #fff; + border-color: #ddd; +} + +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; + line-height: 1.3333333; +} + +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} + +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; +} + +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} + +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} + +.pager li { + display: inline; +} + +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; +} + +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eee; +} + +.pager .next > a, +.pager .next > span { + float: right; +} + +.pager .previous > a, +.pager .previous > span { + float: left; +} + +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #777; + cursor: not-allowed; + background-color: #fff; +} + +.label { + display: inline; + padding: 0.2em 0.6em 0.3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25em; +} + +a.label:hover, +a.label:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} + +.label:empty { + display: none; +} + +.btn .label { + position: relative; + top: -1px; +} + +.label-default { + background-color: #777; +} + +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #5e5e5e; +} + +.label-primary { + background-color: #337ab7; +} + +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #286090; +} + +.label-success { + background-color: #5cb85c; +} + +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} + +.label-info { + background-color: #5bc0de; +} + +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} + +.label-warning { + background-color: #f0ad4e; +} + +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} + +.label-danger { + background-color: #d9534f; +} + +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} + +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: middle; + background-color: #777; + border-radius: 10px; +} + +.badge:empty { + display: none; +} + +.btn .badge { + position: relative; + top: -1px; +} + +.btn-xs .badge, +.btn-group-xs > .btn .badge { + top: 0; + padding: 1px 5px; +} + +a.badge:hover, +a.badge:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} + +.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #337ab7; + background-color: #fff; +} + +.list-group-item > .badge { + float: right; +} + +.list-group-item > .badge + .badge { + margin-right: 5px; +} + +.nav-pills > li > a > .badge { + margin-left: 3px; +} + +.jumbotron { + padding-top: 30px; + padding-bottom: 30px; + margin-bottom: 30px; + color: inherit; + background-color: #eee; +} + +.jumbotron h1, +.jumbotron .h1 { + color: inherit; +} + +.jumbotron p { + margin-bottom: 15px; + font-size: 21px; + font-weight: 200; +} + +.jumbotron > hr { + border-top-color: #d5d5d5; +} + +.container .jumbotron, +.container-fluid .jumbotron { + padding-right: 15px; + padding-left: 15px; + border-radius: 6px; +} + +.jumbotron .container { + max-width: 100%; +} + +@media screen and (min-width: 768px) { + .jumbotron { + padding-top: 48px; + padding-bottom: 48px; + } + .container .jumbotron, + .container-fluid .jumbotron { + padding-right: 60px; + padding-left: 60px; + } + .jumbotron h1, + .jumbotron .h1 { + font-size: 63px; + } +} +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 20px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: border 0.2s ease-in-out; + -o-transition: border 0.2s ease-in-out; + transition: border 0.2s ease-in-out; +} + +.thumbnail > img, +.thumbnail a > img { + margin-right: auto; + margin-left: auto; +} + +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #337ab7; +} + +.thumbnail .caption { + padding: 9px; + color: #333; +} + +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert h4 { + margin-top: 0; + color: inherit; +} + +.alert .alert-link { + font-weight: bold; +} + +.alert > p, +.alert > ul { + margin-bottom: 0; +} + +.alert > p + p { + margin-top: 5px; +} + +.alert-dismissable, +.alert-dismissible { + padding-right: 35px; +} + +.alert-dismissable .close, +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-success hr { + border-top-color: #c9e2b3; +} + +.alert-success .alert-link { + color: #2b542c; +} + +.alert-info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-info hr { + border-top-color: #a6e1ec; +} + +.alert-info .alert-link { + color: #245269; +} + +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} + +.alert-warning hr { + border-top-color: #f7e1b5; +} + +.alert-warning .alert-link { + color: #66512c; +} + +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} + +.alert-danger hr { + border-top-color: #e4b9c0; +} + +.alert-danger .alert-link { + color: #843534; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #337ab7; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress-striped .progress-bar, +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; +} + +.progress.active .progress-bar, +.progress-bar.active { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-bar-success { + background-color: #5cb85c; +} + +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-info { + background-color: #5bc0de; +} + +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-warning { + background-color: #f0ad4e; +} + +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-danger { + background-color: #d9534f; +} + +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.media { + margin-top: 15px; +} + +.media:first-child { + margin-top: 0; +} + +.media, +.media-body { + overflow: hidden; + zoom: 1; +} + +.media-body { + width: 10000px; +} + +.media-object { + display: block; +} + +.media-object.img-thumbnail { + max-width: none; +} + +.media-right, +.media > .pull-right { + padding-left: 10px; +} + +.media-left, +.media > .pull-left { + padding-right: 10px; +} + +.media-left, +.media-right, +.media-body { + display: table-cell; + vertical-align: top; +} + +.media-middle { + vertical-align: middle; +} + +.media-bottom { + vertical-align: bottom; +} + +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} + +.media-list { + padding-left: 0; + list-style: none; +} + +.list-group { + padding-left: 0; + margin-bottom: 20px; +} + +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; +} + +.list-group-item:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} + +a.list-group-item, +button.list-group-item { + color: #555; +} + +a.list-group-item .list-group-item-heading, +button.list-group-item .list-group-item-heading { + color: #333; +} + +a.list-group-item:hover, +button.list-group-item:hover, +a.list-group-item:focus, +button.list-group-item:focus { + color: #555; + text-decoration: none; + background-color: #f5f5f5; +} + +button.list-group-item { + width: 100%; + text-align: left; +} + +.list-group-item.disabled, +.list-group-item.disabled:hover, +.list-group-item.disabled:focus { + color: #777; + cursor: not-allowed; + background-color: #eee; +} + +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading { + color: inherit; +} + +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text { + color: #777; +} + +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} + +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > .small { + color: inherit; +} + +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #c7ddef; +} + +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; +} + +a.list-group-item-success, +button.list-group-item-success { + color: #3c763d; +} + +a.list-group-item-success .list-group-item-heading, +button.list-group-item-success .list-group-item-heading { + color: inherit; +} + +a.list-group-item-success:hover, +button.list-group-item-success:hover, +a.list-group-item-success:focus, +button.list-group-item-success:focus { + color: #3c763d; + background-color: #d0e9c6; +} + +a.list-group-item-success.active, +button.list-group-item-success.active, +a.list-group-item-success.active:hover, +button.list-group-item-success.active:hover, +a.list-group-item-success.active:focus, +button.list-group-item-success.active:focus { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; +} + +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} + +a.list-group-item-info, +button.list-group-item-info { + color: #31708f; +} + +a.list-group-item-info .list-group-item-heading, +button.list-group-item-info .list-group-item-heading { + color: inherit; +} + +a.list-group-item-info:hover, +button.list-group-item-info:hover, +a.list-group-item-info:focus, +button.list-group-item-info:focus { + color: #31708f; + background-color: #c4e3f3; +} + +a.list-group-item-info.active, +button.list-group-item-info.active, +a.list-group-item-info.active:hover, +button.list-group-item-info.active:hover, +a.list-group-item-info.active:focus, +button.list-group-item-info.active:focus { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} + +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} + +a.list-group-item-warning, +button.list-group-item-warning { + color: #8a6d3b; +} + +a.list-group-item-warning .list-group-item-heading, +button.list-group-item-warning .list-group-item-heading { + color: inherit; +} + +a.list-group-item-warning:hover, +button.list-group-item-warning:hover, +a.list-group-item-warning:focus, +button.list-group-item-warning:focus { + color: #8a6d3b; + background-color: #faf2cc; +} + +a.list-group-item-warning.active, +button.list-group-item-warning.active, +a.list-group-item-warning.active:hover, +button.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus, +button.list-group-item-warning.active:focus { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; +} + +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; +} + +a.list-group-item-danger, +button.list-group-item-danger { + color: #a94442; +} + +a.list-group-item-danger .list-group-item-heading, +button.list-group-item-danger .list-group-item-heading { + color: inherit; +} + +a.list-group-item-danger:hover, +button.list-group-item-danger:hover, +a.list-group-item-danger:focus, +button.list-group-item-danger:focus { + color: #a94442; + background-color: #ebcccc; +} + +a.list-group-item-danger.active, +button.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +button.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus, +button.list-group-item-danger.active:focus { + color: #fff; + background-color: #a94442; + border-color: #a94442; +} + +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} + +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} + +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.panel-body { + padding: 15px; +} + +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} + +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} + +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} + +.panel-title > a, +.panel-title > small, +.panel-title > .small, +.panel-title > small > a, +.panel-title > .small > a { + color: inherit; +} + +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} + +.panel > .list-group, +.panel > .panel-collapse > .list-group { + margin-bottom: 0; +} + +.panel > .list-group .list-group-item, +.panel > .panel-collapse > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; +} + +.panel > .list-group:first-child .list-group-item:first-child, +.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} + +.panel > .list-group:last-child .list-group-item:last-child, +.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} + +.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} + +.list-group + .panel-footer { + border-top-width: 0; +} + +.panel > .table, +.panel > .table-responsive > .table, +.panel > .panel-collapse > .table { + margin-bottom: 0; +} + +.panel > .table caption, +.panel > .table-responsive > .table caption, +.panel > .panel-collapse > .table caption { + padding-right: 15px; + padding-left: 15px; +} + +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} + +.panel > .table:first-child > thead:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} + +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: 3px; +} + +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; +} + +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} + +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} + +.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: 3px; +} + +.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: 3px; +} + +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive, +.panel > .table + .panel-body, +.panel > .table-responsive + .panel-body { + border-top: 1px solid #ddd; +} + +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; +} + +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} + +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; +} + +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; +} + +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} + +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; +} + +.panel > .table-responsive { + margin-bottom: 0; + border: 0; +} + +.panel-group { + margin-bottom: 20px; +} + +.panel-group .panel { + margin-bottom: 0; + border-radius: 4px; +} + +.panel-group .panel + .panel { + margin-top: 5px; +} + +.panel-group .panel-heading { + border-bottom: 0; +} + +.panel-group .panel-heading + .panel-collapse > .panel-body, +.panel-group .panel-heading + .panel-collapse > .list-group { + border-top: 1px solid #ddd; +} + +.panel-group .panel-footer { + border-top: 0; +} + +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #ddd; +} + +.panel-default { + border-color: #ddd; +} + +.panel-default > .panel-heading { + color: #333; + background-color: #f5f5f5; + border-color: #ddd; +} + +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ddd; +} + +.panel-default > .panel-heading .badge { + color: #f5f5f5; + background-color: #333; +} + +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ddd; +} + +.panel-primary { + border-color: #337ab7; +} + +.panel-primary > .panel-heading { + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} + +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #337ab7; +} + +.panel-primary > .panel-heading .badge { + color: #337ab7; + background-color: #fff; +} + +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #337ab7; +} + +.panel-success { + border-color: #d6e9c6; +} + +.panel-success > .panel-heading { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #d6e9c6; +} + +.panel-success > .panel-heading .badge { + color: #dff0d8; + background-color: #3c763d; +} + +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #d6e9c6; +} + +.panel-info { + border-color: #bce8f1; +} + +.panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #bce8f1; +} + +.panel-info > .panel-heading .badge { + color: #d9edf7; + background-color: #31708f; +} + +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #bce8f1; +} + +.panel-warning { + border-color: #faebcc; +} + +.panel-warning > .panel-heading { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} + +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #faebcc; +} + +.panel-warning > .panel-heading .badge { + color: #fcf8e3; + background-color: #8a6d3b; +} + +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #faebcc; +} + +.panel-danger { + border-color: #ebccd1; +} + +.panel-danger > .panel-heading { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} + +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ebccd1; +} + +.panel-danger > .panel-heading .badge { + color: #f2dede; + background-color: #a94442; +} + +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ebccd1; +} + +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} + +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} + +.embed-responsive-16by9 { + padding-bottom: 56.25%; +} + +.embed-responsive-4by3 { + padding-bottom: 75%; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-lg { + padding: 24px; + border-radius: 6px; +} + +.well-sm { + padding: 9px; + border-radius: 3px; +} + +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: 0.2; +} + +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: 0.5; +} + +button.close { + -webkit-appearance: none; + padding: 0; + cursor: pointer; + background: transparent; + border: 0; +} + +.modal-open { + overflow: hidden; +} + +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: none; + overflow: hidden; + -webkit-overflow-scrolling: touch; + outline: 0; +} + +.modal.fade .modal-dialog { + -webkit-transition: -webkit-transform 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + -o-transform: translate(0, -25%); + transform: translate(0, -25%); +} + +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} + +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} + +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} + +.modal-content { + position: relative; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + outline: 0; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000; +} + +.modal-backdrop.fade { + filter: alpha(opacity=0); + opacity: 0; +} + +.modal-backdrop.in { + filter: alpha(opacity=50); + opacity: 0.5; +} + +.modal-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} + +.modal-header .close { + margin-top: -2px; +} + +.modal-title { + margin: 0; + line-height: 1.42857143; +} + +.modal-body { + position: relative; + padding: 15px; +} + +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} + +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} + +@media (min-width: 768px) { + .modal-dialog { + width: 600px; + margin: 30px auto; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + } + .modal-sm { + width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg { + width: 900px; + } +} +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: normal; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; + filter: alpha(opacity=0); + opacity: 0; + line-break: auto; +} + +.tooltip.in { + filter: alpha(opacity=90); + opacity: 0.9; +} + +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} + +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} + +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} + +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} + +.tooltip.top-left .tooltip-arrow { + right: 5px; + bottom: 0; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} + +.tooltip.top-right .tooltip-arrow { + bottom: 0; + left: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} + +.tooltip.bottom-left .tooltip-arrow { + top: 0; + right: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} + +.tooltip.bottom-right .tooltip-arrow { + top: 0; + left: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: normal; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + line-break: auto; +} + +.popover.top { + margin-top: -10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-left: -10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover > .arrow { + border-width: 11px; +} + +.popover > .arrow:after { + content: ""; + border-width: 10px; +} + +.popover.top > .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.top > .arrow:after { + bottom: 1px; + margin-left: -10px; + content: " "; + border-top-color: #fff; + border-bottom-width: 0; +} + +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.right > .arrow:after { + bottom: -10px; + left: 1px; + content: " "; + border-right-color: #fff; + border-left-width: 0; +} + +.popover.bottom > .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, 0.25); +} + +.popover.bottom > .arrow:after { + top: 1px; + margin-left: -10px; + content: " "; + border-top-width: 0; + border-bottom-color: #fff; +} + +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, 0.25); +} + +.popover.left > .arrow:after { + right: 1px; + bottom: -10px; + content: " "; + border-right-width: 0; + border-left-color: #fff; +} + +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + line-height: 1; +} + +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform 0.6s ease-in-out; + -o-transition: -o-transform 0.6s ease-in-out; + transition: transform 0.6s ease-in-out; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000px; + perspective: 1000px; + } + .carousel-inner > .item.next, + .carousel-inner > .item.active.right { + left: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + .carousel-inner > .item.prev, + .carousel-inner > .item.active.left { + left: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right, + .carousel-inner > .item.active { + left: 0; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + background-color: rgba(0, 0, 0, 0); + filter: alpha(opacity=50); + opacity: 0.5; +} + +.carousel-control.left { + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001))); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000", endColorstr="#00000000", GradientType=1); + background-repeat: repeat-x; +} + +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5))); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#00000000", endColorstr="#80000000", GradientType=1); + background-repeat: repeat-x; +} + +.carousel-control:hover, +.carousel-control:focus { + color: #fff; + text-decoration: none; + filter: alpha(opacity=90); + outline: 0; + opacity: 0.9; +} + +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + margin-top: -10px; +} + +.carousel-control .icon-prev, +.carousel-control .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; +} + +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; +} + +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + font-family: serif; + line-height: 1; +} + +.carousel-control .icon-prev:before { + content: "‹"; +} + +.carousel-control .icon-next:before { + content: "›"; +} + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} + +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: #000 \9 ; + background-color: rgba(0, 0, 0, 0); + border: 1px solid #fff; + border-radius: 10px; +} + +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #fff; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} + +.carousel-caption .btn { + text-shadow: none; +} + +@media screen and (min-width: 768px) { + .carousel-control .glyphicon-chevron-left, + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -10px; + font-size: 30px; + } + .carousel-control .glyphicon-chevron-left, + .carousel-control .icon-prev { + margin-left: -10px; + } + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-next { + margin-right: -10px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} +.clearfix:before, +.clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, +.container:before, +.container:after, +.container-fluid:before, +.container-fluid:after, +.row:before, +.row:after, +.form-horizontal .form-group:before, +.form-horizontal .form-group:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after, +.nav:before, +.nav:after, +.navbar:before, +.navbar:after, +.navbar-header:before, +.navbar-header:after, +.navbar-collapse:before, +.navbar-collapse:after, +.pager:before, +.pager:after, +.panel-body:before, +.panel-body:after, +.modal-header:before, +.modal-header:after, +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} + +.clearfix:after, +.dl-horizontal dd:after, +.container:after, +.container-fluid:after, +.row:after, +.form-horizontal .form-group:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after, +.nav:after, +.navbar:after, +.navbar-header:after, +.navbar-collapse:after, +.pager:after, +.panel-body:after, +.modal-header:after, +.modal-footer:after { + clear: both; +} + +.center-block { + display: block; + margin-right: auto; + margin-left: auto; +} + +.pull-right { + float: right !important; +} + +.pull-left { + float: left !important; +} + +.hide { + display: none !important; +} + +.show { + display: block !important; +} + +.invisible { + visibility: hidden; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.hidden { + display: none !important; +} + +.affix { + position: fixed; +} + +@-ms-viewport { + width: device-width; +} +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} + +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} + +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + table.visible-xs { + display: table !important; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} +@media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + table.visible-sm { + display: table !important; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + table.visible-md { + display: table !important; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } +} +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + table.visible-lg { + display: table !important; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} +@media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } +} +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } +} +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } +} +.visible-print { + display: none !important; +} + +@media print { + .visible-print { + display: block !important; + } + table.visible-print { + display: table !important; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } +} +.visible-print-block { + display: none !important; +} + +@media print { + .visible-print-block { + display: block !important; + } +} +.visible-print-inline { + display: none !important; +} + +@media print { + .visible-print-inline { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; +} + +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} +@media print { + .hidden-print { + display: none !important; + } +} + +@media (min-width: 1200px) { + .container { + width: 970px; + } +} +a { + color: #459CE7; +} + +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + background-color: #5585B3; + border-color: #4477A3; +} + +.pagination > li > a, +.pagination > li > span { + -webkit-transition: all 0.25s linear; + -moz-transition: all 0.25s linear; + -ms-transition: all 0.25s linear; + -o-transition: all 0.25s linear; + transition: all 0.25s linear; +} + +/* config variables */ +/* roboto font */ +/* source sans pro */ +/* lato */ +/* merriweather */ +/* karla */ +/* layout */ +body { + font-family: "Roboto"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a:focus, button:focus { + outline: none; +} + +.navbar.normal { + min-height: 70px; + background: #223741; + border-radius: 0px; + box-shadow: 0px 1px 3px 1px rgba(46, 46, 46, 0.3), inset rgba(255, 255, 255, 0.2) 0 1px 1px; + border: none; + z-index: 9999; +} +.navbar.normal .navbar-header .navbar-brand { + color: #fff; + padding: 25px 15px; + font-weight: 400; + font-size: 23px; +} +.navbar.normal .navbar-header .navbar-toggle { + margin-top: 17px; +} +@media (min-width: 768px) { + .navbar.normal .navbar-collapse { + text-align: center; + } +} +@media (min-width: 768px) { + .navbar.normal .navbar-collapse .navbar-nav { + float: none; + display: inline-block; + } +} +.navbar.normal .navbar-collapse .navbar-nav > li { + float: none; + display: inline-block; +} +@media (max-width: 767px) { + .navbar.normal .navbar-collapse .navbar-nav > li { + display: block; + } +} +.navbar.normal .navbar-collapse .navbar-nav > li.active > a { + background: none; + color: #fff; +} +.navbar.normal .navbar-collapse .navbar-nav > li > a { + padding: 25px 20px; + color: #d5d5d5; + font-weight: 500; + z-index: 1001; + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -ms-transition: color 0.2s linear; + -o-transition: color 0.2s linear; + transition: color 0.2s linear; +} +@media (max-width: 767px) { + .navbar.normal .navbar-collapse .navbar-nav > li > a { + padding: 18px 20px; + } +} +.navbar.normal .navbar-collapse .navbar-nav > li > a:hover { + color: #fff; +} +@media (max-width: 767px) { + .navbar.normal .navbar-collapse .navbar-nav > li.open ul.dropdown-menu a { + color: #fff; + padding: 12px 18px 12px 30px; + } +} +.navbar.normal .navbar-collapse .navbar-nav > li ul.dropdown-menu { + text-align: left; + padding: 9px 0; +} +.navbar.normal .navbar-collapse .navbar-nav > li ul.dropdown-menu li a { + padding: 8px 18px; +} +.navbar.normal .navbar-collapse .navbar-nav .button { + padding: 8px 18px; + font-size: 13px; + position: relative; + top: 17px; +} +@media (max-width: 767px) { + .navbar.normal .navbar-collapse .navbar-nav .button { + box-shadow: none; + background: none; + top: 0; + margin-bottom: 20px; + color: #d5d5d5 !important; + font-size: 15px; + } +} +.navbar.hero { + margin: 0px; + border-radius: 0px; + border: 0px; + z-index: 999; + position: absolute; + width: 100%; + top: 0; +} +@media (min-width: 768px) { + .navbar.hero { + background: transparent; + padding-top: 10px; + } + .navbar.hero .navbar-nav > .active > a { + background: transparent; + color: #fff; + } +} +.navbar.hero .navbar-brand { + color: #fff; + font-weight: 400; + font-size: 26px; +} +.navbar.hero .navbar-nav > li.dropdown > a .caret { + border-top-color: #EBEBEB; +} +.navbar.hero .navbar-nav > li > a { + color: #fff; + font-size: 15px; + padding: 15px 20px; + font-weight: 500; + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -ms-transition: color 0.2s linear; + -o-transition: color 0.2s linear; + transition: color 0.2s linear; +} +.navbar.hero .navbar-nav > li > a:hover { + color: #fff; +} +.navbar.hero .navbar-nav > li.open > a { + color: #fff !important; + background-color: rgba(0, 0, 0, 0.8); + border-radius: 4px 4px 0 0; +} +@media (max-width: 767px) { + .navbar.hero .navbar-nav > li.open ul.dropdown-menu a { + color: #fff; + padding: 12px 18px 12px 30px; + } +} +.navbar.hero .navbar-nav > li ul.dropdown-menu { + text-align: left; + background: rgba(0, 0, 0, 0.8); + box-shadow: none; + padding: 5px 0 8px 0; + border-radius: 4px 0 4px 4px; + border: 0px; +} +.navbar.hero .navbar-nav > li ul.dropdown-menu li a { + color: #fff; + padding: 10px 18px; +} +.navbar.hero .navbar-nav > li ul.dropdown-menu li a:hover { + color: #e9e9e9; + background: none; +} +.navbar.white { + min-height: 70px; + margin-bottom: 0; + border-radius: 0; + border: 0; + z-index: 999; +} +@media (min-width: 768px) { + .navbar.white { + background: #fff; + box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.75); + } +} +@media (min-width: 1200px) { + .navbar.white .container { + width: 1000px; + } +} +.navbar.white .navbar-header .navbar-brand { + color: #333; + font-weight: 400; + font-size: 26px; + padding-top: 24px; +} +@media (max-width: 767px) { + .navbar.white .navbar-header .navbar-brand { + color: #fff; + font-size: 20px; + } +} +.navbar.white .navbar-header .navbar-toggle { + margin-top: 17px; +} +@media (min-width: 768px) { + .navbar.white .navbar-collapse { + text-align: center; + } +} +@media (min-width: 768px) { + .navbar.white .navbar-collapse .navbar-nav { + float: none; + display: inline-block; + } +} +.navbar.white .navbar-collapse .navbar-nav > li { + float: none; + display: inline-block; +} +@media (max-width: 767px) { + .navbar.white .navbar-collapse .navbar-nav > li { + display: block; + } +} +.navbar.white .navbar-collapse .navbar-nav > li.active > a { + background: none; + color: #333; +} +.navbar.white .navbar-collapse .navbar-nav > li > a { + padding: 25px 20px; + color: #555; + z-index: 1001; + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -ms-transition: color 0.2s linear; + -o-transition: color 0.2s linear; + transition: color 0.2s linear; +} +.navbar.white .navbar-collapse .navbar-nav > li > a.signup { + border: 1px solid #CED7DF; + border-radius: 5px; + padding: 8px 11px; + font-size: 13px; + color: #333; + margin-left: 20px; +} +@media (max-width: 767px) { + .navbar.white .navbar-collapse .navbar-nav > li > a { + padding: 18px 20px; + color: #fff; + } +} +.navbar.white .navbar-collapse .navbar-nav > li > a .fa-chevron-down { + color: #777; + font-size: 8px; + margin-left: 2px; + position: relative; + top: -1px; +} +.navbar.white .navbar-collapse .navbar-nav > li > a:hover { + color: #333; +} +@media (max-width: 767px) { + .navbar.white .navbar-collapse .navbar-nav > li > a:hover { + color: #fff; + } +} +@media (min-width: 768px) { + .navbar.white .navbar-collapse .navbar-nav > li.open > a { + background: #fff; + color: #333; + } +} +.navbar.white .navbar-collapse .navbar-nav > li.open > a .caret { + border-top-color: #999; + border-bottom-color: #999; +} +@media (max-width: 767px) { + .navbar.white .navbar-collapse .navbar-nav > li.open ul.dropdown-menu a { + color: #fff; + padding: 12px 18px 12px 30px; + } +} +.navbar.white .navbar-collapse .navbar-nav > li ul.dropdown-menu { + text-align: left; + border: 0; + padding: 0 0 10px; + box-shadow: 0 8px 12px rgba(0, 0, 0, 0.175); +} +.navbar.white .navbar-collapse .navbar-nav > li ul.dropdown-menu li a { + padding: 8px 18px; +} + +.main-footer { + background: #414B5B; + margin-top: 120px; + padding-top: 60px; + padding-bottom: 30px; +} +@media (max-width: 991px) { + .main-footer { + padding-top: 30px; + } +} +@media (max-width: 767px) { + .main-footer .menu { + text-align: center; + margin-top: 40px; + } +} +.main-footer .menu h3 { + margin-top: 0; + color: #ced5e0; + text-transform: uppercase; + letter-spacing: 4px; + font-size: 14px; + font-weight: 600; +} +.main-footer .menu ul { + list-style-type: none; + padding: 0; + margin-top: 30px; +} +.main-footer .menu ul li { + display: block; + margin-bottom: 9px; +} +.main-footer .menu ul li a { + color: #fff; + display: inline-block; + font-weight: 500; + font-size: 13px; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.main-footer .menu ul li a:hover { + color: #f7f7f7; + text-decoration: none; +} +@media (max-width: 991px) { + .main-footer .menu ul li a { + padding: 0; + } +} +.main-footer .menu ul li .hiring { + font-weight: 700; + letter-spacing: 0.3px; + text-transform: uppercase; + word-spacing: 1.5px; + font-size: 11px; + margin-left: 4px; + padding: 3px 8px; + border-radius: 4px; + color: #fff; + background: #49B7E2; +} +.main-footer .menu ul li .hiring:hover { + color: #fff; + background: #0E82AF; +} +.main-footer .newsletter { + text-align: right; +} +@media (max-width: 767px) { + .main-footer .newsletter { + text-align: center; + margin-top: 40px; + } +} +.main-footer .newsletter .signup { + margin-bottom: 40px; +} +.main-footer .newsletter .signup p { + font-size: 13px; + color: #fff; + width: 90%; + float: right; + text-align: left; + line-height: 20px; +} +@media (max-width: 767px) { + .main-footer .newsletter .signup p { + float: none; + text-align: center; + width: 100%; + } +} +.main-footer .newsletter .signup form { + float: right; + width: 90%; + position: relative; +} +@media (max-width: 767px) { + .main-footer .newsletter .signup form { + float: none; + width: 100%; + } +} +.main-footer .newsletter .signup form input[type=text] { + font-size: 13px; +} +.main-footer .newsletter .signup form input[type=text]::-webkit-input-placeholder { + font-size: 13px; + line-height: 17px; +} +.main-footer .newsletter .signup form input[type=text]:-moz-placeholder { + font-size: 13px; + line-height: 17px; +} +.main-footer .newsletter .signup form input[type=text]::-moz-placeholder { + font-size: 13px; + line-height: 17px; +} +.main-footer .newsletter .signup form input[type=text]:-ms-input-placeholder { + font-size: 13px; + line-height: 17px; +} +.main-footer .newsletter .signup form input[type=submit] { + border: 0; + background: #84B0E2; + color: #FFF; + border-radius: 3px; + padding: 6px 10px; + position: absolute; + top: 5px; + text-shadow: 1px 1px rgba(0, 0, 0, 0.28); + right: 5px; + font-size: 13px; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.main-footer .newsletter .signup form input[type=submit]:hover { + background: #6284AC; +} +.main-footer .newsletter a { + border: 0; + text-decoration: none; + opacity: 1; + filter: alpha(opacity=100); + margin-left: 15px; + position: relative; + top: -10px; + -webkit-transition: all 0.3s ease; + -moz-transition: all 0.3s ease; + -ms-transition: all 0.3s ease; + -o-transition: all 0.3s ease; + transition: all 0.3s ease; +} +.main-footer .newsletter a:hover { + opacity: 0.7; + filter: alpha(opacity=70); +} +.main-footer .credits { + margin-top: 30px; +} +.main-footer .credits .col-md-12 { + padding-top: 20px; + font-size: 13px; + text-align: center; + color: #a3aab5; +} +.main-footer--white { + background: #fff; + margin-top: 0; +} +.main-footer--white .menu h3 { + color: #888; +} +.main-footer--white .menu ul li a { + color: #89A1C0; +} +.main-footer--white .menu ul li a:hover { + color: #000; +} +.main-footer--white .newsletter .signup p { + color: #666; +} +.main-footer--white .credits .col-md-12 { + color: #A1AAB6; +} + +/* snippets */ +.features-section { + margin-top: 90px; +} +.features-section--margin { + margin-top: 120px; +} +.features-section--margin .feature { + margin-bottom: 110px; +} +.features-section--margin .feature.last { + margin-bottom: 50px; +} +@media (max-width: 991px) { + .features-section { + margin-top: 50px; + } +} +.features-section .header { + text-align: center; +} +.features-section .header h2 { + color: #555; + font-weight: 400; + font-size: 27px; +} +@media (max-width: 767px) { + .features-section .header h2 { + font-size: 25px; + } +} +.features-section .header p { + font-size: 17px; + color: #757575; +} +.features-section .feature { + margin-top: 60px; +} +.features-section .feature.last .image img { + margin-left: 70px; +} +@media (min-width: 992px) { + .features-section .feature.backwards .info { + float: right; + } + .features-section .feature.backwards .image { + float: left; + } + .features-section .feature.backwards .image img { + margin: 0 auto; + } +} +@media (max-width: 991px) { + .features-section .feature { + margin-top: 10px; + } +} +.features-section .feature .info h4 { + color: #3B88B6; + font-weight: normal; + font-size: 19px; + line-height: 25px; + margin-top: 35px; +} +@media (max-width: 991px) { + .features-section .feature .info h4 { + text-align: center; + } +} +.features-section .feature .info p { + color: #6C7279; + font-size: 14px; + margin-top: 20px; + line-height: 24px; + width: 83%; + font-weight: normal; +} +@media (max-width: 991px) { + .features-section .feature .info p { + text-align: center; + width: 100%; + font-size: 14px; + } +} +@media (max-width: 991px) { + .features-section .feature .image img { + margin: 0 auto !important; + margin-top: 20px; + } +} +.features-section .divider { + height: 1px; + width: 100%; + background-color: #ddd; + position: relative; + margin: 70px 0 60px; +} +.features-section .divider:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 2px; + background-image: -webkit-gradient(linear, left top, right top, color-stop(0, rgba(255, 255, 255, 0.75)), color-stop(0.5, rgba(250, 250, 250, 0)), color-stop(1, rgba(255, 255, 255, 0.75))); + background-image: -webkit-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: -moz-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: -o-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: -ms-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); +} +@media (max-width: 991px) { + .features-section .divider { + margin: 55px 0 35px; + } +} + +.slider-section { + margin-top: 70px; +} +@media (max-width: 991px) { + .slider-section { + margin-top: 30px; + } +} +.slider-section .header { + text-align: center; +} +.slider-section .header h3 { + font-size: 25px; + color: #555; + line-height: 25px; + font-weight: 400; + margin-bottom: 15px; +} +.slider-section .slide-wrapper { + border-bottom: 1px solid #E4E4E4; +} +.slider-section .slide-wrapper .slideshow { + background: url("../../images/slider-bg.png") 0 0 no-repeat; + margin: 0 auto; + margin-top: 35px; + width: 844px; + height: 418px; + position: relative; + overflow: hidden; +} +@media (max-width: 991px) { + .slider-section .slide-wrapper .slideshow { + background: none; + width: inherit; + height: 340px; + } +} +@media (max-width: 620px) { + .slider-section .slide-wrapper .slideshow { + height: 290px; + } +} +@media (max-width: 530px) { + .slider-section .slide-wrapper .slideshow { + height: 240px; + } +} +@media (max-width: 450px) { + .slider-section .slide-wrapper .slideshow { + height: 205px; + } +} +.slider-section .slide-wrapper .slideshow .btn-nav { + width: 48px; + height: 51px; + position: absolute; + top: 55%; + z-index: 999; + cursor: pointer; + opacity: 0; + filter: alpha(opacity=0); + -webkit-transition: all 0.3s ease; + -moz-transition: all 0.3s ease; + -o-transition: all 0.3s ease; + -ms-transition: all 0.3s ease; + transition: all 0.3s ease; +} +.slider-section .slide-wrapper .slideshow .btn-nav.active { + opacity: 1; + filter: alpha(opacity=100); + -moz-transform: translateX(0px) !important; + -o-transform: translateX(0px) !important; + -ms-transform: translateX(0px) !important; + -webkit-transform: translateX(0px) !important; + transform: translateX(0px) !important; +} +.slider-section .slide-wrapper .slideshow .btn-nav.prev { + background: url("../../images/slider/slide-left.png") 0 0 no-repeat; + left: 91px; + -moz-transform: translateX(-48px); + -o-transform: translateX(-48px); + -ms-transform: translateX(-48px); + -webkit-transform: translateX(-48px); + transform: translateX(-48px); +} +@media (max-width: 991px) { + .slider-section .slide-wrapper .slideshow .btn-nav.prev { + left: 30px; + } +} +.slider-section .slide-wrapper .slideshow .btn-nav.next { + background: url("../../images/slider/slide-right.png") 0 0 no-repeat; + right: 91px; + -moz-transform: translateX(48px); + -o-transform: translateX(48px); + -ms-transform: translateX(48px); + -webkit-transform: translateX(48px); + transform: translateX(48px); +} +@media (max-width: 991px) { + .slider-section .slide-wrapper .slideshow .btn-nav.next { + right: 29px; + } +} +.slider-section .slide-wrapper .slideshow .slide { + width: 100%; + height: 100%; + position: absolute; +} +.slider-section .slide-wrapper .slideshow .slide.active img { + opacity: 1; + filter: alpha(opacity=100); + -moz-transform: translateY(0px); + -o-transform: translateY(0px); + -ms-transform: translateY(0px); + -webkit-transform: translateY(0px); + transform: translateY(0px); +} +.slider-section .slide-wrapper .slideshow .slide img { + position: absolute; + bottom: 0; + left: 0; + right: 0; + margin: auto; + -moz-transform: translateY(300px); + -o-transform: translateY(300px); + -ms-transform: translateY(300px); + -webkit-transform: translateY(300px); + transform: translateY(300px); + opacity: 0; + -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0); + filter: alpha(opacity=0); + -webkit-transition: all 0.6s ease; + -moz-transition: all 0.6s ease; + -o-transition: all 0.6s ease; + -ms-transition: all 0.6s ease; + transition: all 0.6s ease; +} +@media (max-width: 767px) { + .slider-section .slide-wrapper .slideshow .slide img { + width: 100%; + } +} + +/* pages */ +.index-hero { + overflow: hidden; + background: #4B4848; + background-size: cover; + background-image: url("../../images/8254861601_1fbf969a14_b.jpg"); + /*background: -webkit-radial-gradient(closest-corner, rgba(16, 47, 70, 0) 60%, rgba(16, 47, 70, 0.26)), -webkit-linear-gradient(108deg, #5E8199, #153752 90%);*/ + background-position: center 16%; + height: 580px; + position: relative; + padding-top: 150px; + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4); +} +@media (max-width: 991px) { + .index-hero { + padding-top: 100px; + } +} +@media (max-width: 767px) { + .index-hero { + height: 550px; + } +} +.index-hero:before { + position: absolute; + content: ""; + left: 0; + bottom: 0; + width: 100%; + height: 100%; + background: -webkit-linear-gradient(right, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0) 1%, rgba(0, 0, 0, 0.1) 26%, rgba(0, 0, 0, 0.35) 71%, rgba(0, 0, 0, 0.5) 100%); + background: -moz-linear-gradient(right, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0) 1%, rgba(0, 0, 0, 0.1) 26%, rgba(0, 0, 0, 0.35) 71%, rgba(0, 0, 0, 0.5) 100%); + background: -o-linear-gradient(right, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0) 1%, rgba(0, 0, 0, 0.1) 26%, rgba(0, 0, 0, 0.35) 71%, rgba(0, 0, 0, 0.5) 100%); + background: linear-gradient(right, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0) 1%, rgba(0, 0, 0, 0.1) 26%, rgba(0, 0, 0, 0.35) 71%, rgba(0, 0, 0, 0.5) 100%); +} +.index-hero:after { + position: absolute; + content: ""; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(32, 54, 114, 0.29); +} +.index-hero .container { + position: relative; + z-index: 33; +} +.index-hero h1.hero-text { + line-height: 52px; + text-align: left; + font-size: 37px; + text-transform: uppercase; + font-weight: 300; + letter-spacing: 0.3px; + color: #FFF; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); + -webkit-animation-duration: 1s; +} +@media (max-width: 767px) { + .index-hero h1.hero-text { + font-size: 25px; + line-height: 38px; + margin-top: 0; + text-align: center; + } +} +.index-hero .sub-text { + width: 50%; + margin-top: 25px; + color: #fff; + font-weight: 400; + font-size: 17px; + line-height: 28px; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); + -webkit-animation-duration: 1s; +} +@media (max-width: 991px) { + .index-hero .sub-text { + width: 70%; + } +} +@media (max-width: 767px) { + .index-hero .sub-text { + font-size: 15px; + width: 100%; + text-align: center; + } +} +.index-hero .cta { + -webkit-animation-delay: 0.6s; + -moz-animation-delay: 0.6s; + -o-animation-delay: 0.6s; + -ms-animation-delay: 0.6s; + animation-delay: 0.6s; + margin-top: 60px; +} +@media (max-width: 767px) { + .index-hero .cta { + text-align: center; + } +} +.index-hero .cta a { + margin-right: 20px; + padding: 15px 31px; +} +@media (max-width: 767px) { + .index-hero .cta a { + font-size: 14px; + padding: 12px 25px; + margin: 0px 15px 15px 0px; + } +} +.index-hero .img { + position: absolute; + top: 15px; + left: 625px; + width: 570px; + height: 333px; + background-image: url("../../images/static-hero.png"); + background-repeat: no-repeat; +} +@media (max-width: 991px) { + .index-hero .img { + display: none; + } +} + +.testimonials-section { + margin-top: 100px; +} +.testimonials-section .header { + text-align: center; +} +.testimonials-section .header h3 { + font-size: 23px; + color: #656565; + font-weight: 400; + line-height: 25px; + margin-bottom: 45px; +} +.testimonials-section .testimonial { + width: 90%; +} +@media (max-width: 767px) { + .testimonials-section .testimonial { + float: none !important; + margin: 0 auto; + margin-bottom: 45px; + } +} +.testimonials-section .testimonial .quote { + color: #53565f; + line-height: 26px; + border: 1px solid #E2E2E2; + padding: 16px 25px; + border-radius: 5px; + width: 95%; + position: relative; +} +@media (max-width: 767px) { + .testimonials-section .testimonial .quote { + margin: 0 auto; + } +} +.testimonials-section .testimonial .quote .arrow-down { + position: absolute; + bottom: 3px; + left: 30px; +} +.testimonials-section .testimonial .quote .arrow-down .arrow, .testimonials-section .testimonial .quote .arrow-down .arrow-border { + border-color: #fff transparent transparent; + border-style: solid; + border-width: 11px; + cursor: pointer; + position: absolute; + top: 3px; + z-index: 1002; +} +.testimonials-section .testimonial .quote .arrow-down .arrow-border { + border-color: #E2E2E2 transparent transparent; + border-width: 12px; + top: 3px; + z-index: 1001; + left: -1px; +} +.testimonials-section .testimonial .author { + margin-top: 40px; + margin-left: 10px; +} +@media (max-width: 767px) { + .testimonials-section .testimonial .author { + margin-left: 6%; + } +} +.testimonials-section .testimonial .author .pic { + width: 71px; + height: 71px; + border-radius: 50px; + float: left; + position: relative; + top: -12px; + margin-right: 18px; + border: 1px solid #ccc; +} +.testimonials-section .testimonial .author .name { + color: #3C92C5; + line-height: 23px; + font-weight: 500; +} +.testimonials-section .testimonial .author .company { + font-size: 14px; + color: #909090; + line-height: 23px; + font-weight: 400; +} + +.cta-section { + text-align: center; + margin-top: 120px; +} +.cta-section p { + font-size: 18px; + color: #7A7D81; + letter-spacing: 0.1px; +} +.cta-section a { + display: inline-block; + margin-top: 30px; + background: #6CAAE4; + color: #FFF; + padding: 23px 40px; + font-size: 16px; + letter-spacing: 1px; + font-weight: 500; + text-transform: uppercase; + border-radius: 5px; + box-shadow: rgba(23, 43, 99, 0.3) 0 7px 28px; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.cta-section a:hover { + text-decoration: none; + background: #3FD8DF; +} + +.clients-section { + background: #5E7A9B; + background: radial-gradient(#36414E, #253241); + margin-top: 120px; + padding: 65px 0 80px; + text-align: center; +} +.clients-section h3 { + color: #FFF; + margin-top: 0; + font-weight: 300; + font-size: 35px; + letter-spacing: 0.3px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} +.clients-section p { + font-size: 16px; + color: #f8f8f8; + margin-top: 17px; + font-weight: 300; + line-height: 25px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} +.clients-section .logos { + margin-top: 35px; +} +.clients-section .logos img { + margin: 0 16px; +} +@media (max-width: 767px) { + .clients-section .logos img { + margin: 0 auto; + display: block; + margin-bottom: 15px; + } +} + +@-webkit-keyframes $name { + 0% { + left: 0; + } + 100% { + left: -15px; + } +} +@-moz-keyframes $name { + 0% { + left: 0; + } + 100% { + left: -15px; + } +} +@-o-keyframes $name { + 0% { + left: 0; + } + 100% { + left: -15px; + } +} +@keyframes $name { + 0% { + left: 0; + } + 100% { + left: -15px; + } +} +.slider-hero { + background: #4B4848; + height: 585px; + position: relative; + /* prev & next arrows */ + /* navigation dots */ + /* slides */ + /* video modal */ +} +@media (max-width: 991px) { + .slider-hero { + padding-top: 100px; + } +} +@media (max-width: 767px) { + .slider-hero { + height: 550px; + } +} +.slider-hero .slide-nav { + position: absolute; + top: 0; + bottom: 0; + width: 140px; + z-index: 200; + text-indent: 100%; + overflow: hidden; + opacity: 0.5; + filter: alpha(opacity=50); + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -ms-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; +} +.slider-hero .slide-nav:hover { + opacity: 1; + filter: alpha(opacity=100); +} +.slider-hero .slide-nav.prev:after { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + width: 19px; + height: 58px; + background: url("../../images/slide-arrow.png") no-repeat; + left: 30px; + -moz-transform: rotate(180deg); + -o-transform: rotate(180deg); + -ms-transform: rotate(180deg); + -webkit-transform: rotate(180deg); + transform: rotate(180deg); +} +.slider-hero .slide-nav.next { + right: 0; +} +.slider-hero .slide-nav.next:after { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + width: 19px; + height: 58px; + background: url("../../images/slide-arrow.png") no-repeat; + right: 30px; +} +.slider-hero nav { + z-index: 999; + position: absolute; + bottom: 20px; + width: 100%; + text-align: center; +} +.slider-hero nav a { + display: inline-block; + width: 10px; + height: 10px; + background: #a2a2a2; + border-radius: 50%; + margin: 0 3px; +} +.slider-hero nav a.active { + background: #fff; +} +.slider-hero .slides { + overflow: hidden; + position: absolute; + top: 0px; + width: 100%; + height: 100%; + /* each slide */ +} +.slider-hero .slides .slide { + display: none; + z-index: 80; + height: 100%; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + padding-top: 90px; +} +.slider-hero .slides .slide.active { + opacity: 1; + filter: alpha(opacity=100); + display: block; + z-index: 100; +} +.slider-hero .slides .slide.next { + z-index: 90; +} +@media (min-width: 768px) { + .slider-hero .slides .slide.next .bg { + -webkit-animation: slideLeft 700ms linear; + -moz-animation: slideLeft 700ms linear; + -o-animation: slideLeft 700ms linear; + -ms-animation: slideLeft 700ms linear; + animation: slideLeft 700ms linear; + } +} +.slider-hero .slides .slide .container { + position: relative; + z-index: 3; + height: 100%; +} +.slider-hero .slides .slide .bg { + position: absolute; + top: 0; + left: -15px; + width: 102%; + height: 100%; + background-position: center center; + background-size: cover; +} +@media (max-width: 767px) { + .slider-hero .slides .slide .bg { + left: 0px; + } +} +.slider-hero .slides .slide .bg:before { + position: absolute; + top: 10%; + bottom: 0; + left: 0; + right: 0; + content: ""; + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(0, 0, 0, 0)), color-stop(100%, rgba(0, 0, 0, 0.25))); + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0.25) 100%); + background: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.25)); + background: -o-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.25)); + background: linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.25)); +} +.slider-hero .slides .slide .bg:after { + content: ""; + position: absolute; + display: block; + height: 100%; + left: 0; + width: 100%; + top: 0; +} +@media (max-width: 767px) { + .slider-hero .slides .slide.first { + padding-top: 40px; + } +} +.slider-hero .slides .slide.first .bg { + background-image: url("../../images/bgs/photo-1467703834117-04386e3dadd8.jpeg"); + background-position: center 29%; +} +.slider-hero .slides .slide.first .bg:after { + background: rgba(5, 11, 29, 0.4); +} +.slider-hero .slides .slide.first h1.hero-text { + text-align: center; + font-family: "Lato", "Helvetica Neue", Arial; + font-size: 37px; + font-weight: 400; + color: #fff; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.65); +} +@media (max-width: 767px) { + .slider-hero .slides .slide.first h1.hero-text { + font-size: 25px; + } +} +.slider-hero .slides .slide.first .sub-text { + margin: 0 auto; + font-family: "Lato", "Helvetica Neue", Arial; + line-height: 26px; + margin-top: 25px; + text-align: center; + color: #fff; + font-weight: 400; + font-size: 17px; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2); + width: 50%; + -webkit-animation-delay: 0.6s; + -moz-animation-delay: 0.6s; + -o-animation-delay: 0.6s; + -ms-animation-delay: 0.6s; + animation-delay: 0.6s; +} +@media (max-width: 991px) { + .slider-hero .slides .slide.first .sub-text { + width: 70%; + } +} +@media (max-width: 767px) { + .slider-hero .slides .slide.first .sub-text { + font-size: 15px; + } +} +.slider-hero .slides .slide.first .video-wrapper { + text-align: center; + margin-top: 50px; +} +@media (max-width: 767px) { + .slider-hero .slides .slide.first .video-wrapper { + margin-top: 25px; + } +} +.slider-hero .slides .slide.first .video-wrapper .video { + display: inline-block; + padding: 6px; + background: rgba(255, 255, 255, 0.5); + border-radius: 5px; + -webkit-animation-duration: 1.2s; + -moz-animation-duration: 1.2s; + -o-animation-duration: 1.2s; + animation-duration: 1.2s; +} +.slider-hero .slides .slide.first .video-wrapper .video img { + max-width: 280px; + cursor: pointer; +} +@media (max-width: 767px) { + .slider-hero .slides .slide.first .video-wrapper .video img { + max-width: 200px; + } +} +@media (max-width: 767px) { + .slider-hero .slides .slide.second { + padding-top: 50px; + } +} +.slider-hero .slides .slide.second .bg { + background-image: url("../../images/bgs/photo-1472132858735-6313c7962473.jpeg"); + background-position: center 18%; +} +.slider-hero .slides .slide.second .bg:after { + background: rgba(52, 52, 58, 0.5); +} +.slider-hero .slides .slide.second .info h1.hero-text { + margin-top: 65px; + font-family: "Lato", "Helvetica Neue", Arial; + font-size: 40px; + line-height: 45px; + font-weight: 400; + color: #fff; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8); +} +@media (max-width: 767px) { + .slider-hero .slides .slide.second .info h1.hero-text { + font-size: 28px; + text-align: center; + } +} +.slider-hero .slides .slide.second .info p { + color: rgba(255, 255, 255, 0.94); + font-family: "Lato", "Helvetica Neue", Arial; + letter-spacing: 0.3px; + margin-top: 25px; + font-size: 17px; + line-height: 26px; +} +.slider-hero .slides .slide.second .info .cta { + -webkit-animation-delay: 0.6s; + -moz-animation-delay: 0.6s; + -o-animation-delay: 0.6s; + -ms-animation-delay: 0.6s; + animation-delay: 0.6s; + margin-top: 40px; +} +@media (max-width: 767px) { + .slider-hero .slides .slide.second .info .cta { + text-align: center; + } +} +.slider-hero .slides .slide.second .info .cta a { + position: relative; + margin: 0px 30px 0 0; + padding: 16px 25px; + letter-spacing: 1px; + font-weight: 500; + text-transform: uppercase; + border: 2px solid rgba(255, 255, 255, 0.8); + background: rgba(0, 0, 0, 0.4); +} +.slider-hero .slides .slide.second .info .cta a:hover { + color: #252525; + background: #fff; + padding-right: 45px; +} +.slider-hero .slides .slide.second .info .cta a:hover .fa { + opacity: 1; +} +.slider-hero .slides .slide.second .info .cta a .fa { + opacity: 0; + font-size: 13px; + position: absolute; + color: #252525; + top: 21px; + right: 20px; + -webkit-transition: all 0.15s linear; + -moz-transition: all 0.15s linear; + -ms-transition: all 0.15s linear; + -o-transition: all 0.15s linear; + transition: all 0.15s linear; +} +@media (max-width: 767px) { + .slider-hero .slides .slide.second .info .cta a { + font-size: 14px; + padding: 12px 25px; + margin: 0px 15px 15px 0px; + } +} +.slider-hero .slides .slide.second .mobiles img { + display: block; + margin: 0 auto; + width: 360px; + -webkit-animation-duration: 1.2s; + -moz-animation-duration: 1.2s; + -o-animation-duration: 1.2s; + animation-duration: 1.2s; +} +.slider-hero .slides .slide.third { + padding-top: 115px; +} +@media (max-width: 767px) { + .slider-hero .slides .slide.third { + padding-top: 50px; + } +} +.slider-hero .slides .slide.third .bg { + background-image: url("../../images/bgs/photo-1467659226669-a1360d97be2d.jpeg"); + background-position: center 15%; +} +.slider-hero .slides .slide.third .bg:after { + background: rgba(3, 9, 27, 0.5); +} +.slider-hero .slides .slide.third h1, .slider-hero .slides .slide.third .sub-text { + -webkit-animation-duration: 1s; + -moz-animation-duration: 1s; + -o-animation-duration: 1s; + animation-duration: 1s; +} +.slider-hero .slides .slide.third h1.hero-text { + text-align: center; + font-size: 38px; + font-weight: 400; + color: #fff; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} +@media (max-width: 767px) { + .slider-hero .slides .slide.third h1.hero-text { + font-size: 28px; + } +} +.slider-hero .slides .slide.third .sub-text { + margin: 0 auto; + margin-top: 25px; + text-align: center; + color: #f7f7f7; + font-weight: 400; + font-size: 17px; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2); + width: 50%; +} +@media (max-width: 991px) { + .slider-hero .slides .slide.third .sub-text { + width: 70%; + } +} +@media (max-width: 767px) { + .slider-hero .slides .slide.third .sub-text { + font-size: 15px; + } +} +.slider-hero .slides .slide.third .cta { + -webkit-animation-delay: 0.6s; + -moz-animation-delay: 0.6s; + -o-animation-delay: 0.6s; + -ms-animation-delay: 0.6s; + animation-delay: 0.6s; + text-align: center; + margin-top: 60px; +} +.slider-hero .slides .slide.third .cta a { + margin: 0px 20px; +} +@media (max-width: 767px) { + .slider-hero .slides .slide.third .cta a { + font-size: 14px; + padding: 12px 25px; + margin: 0px 15px 15px 0px; + } +} +.slider-hero .video-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 999; + opacity: 0; + filter: alpha(opacity=0); + visibility: hidden; + -webkit-transition: all 0.25s linear; + -moz-transition: all 0.25s linear; + -ms-transition: all 0.25s linear; + -o-transition: all 0.25s linear; + transition: all 0.25s linear; +} +.slider-hero .video-modal.active { + opacity: 1; + filter: alpha(opacity=100); + visibility: visible; + background: rgba(44, 44, 45, 0.8); +} +.slider-hero .video-modal.active .wrap { + -moz-transform: scale3d(1, 1, 1); + -o-transform: scale3d(1, 1, 1); + -ms-transform: scale3d(1, 1, 1); + -webkit-transform: scale3d(1, 1, 1); + transform: scale3d(1, 1, 1); + opacity: 1; + filter: alpha(opacity=100); +} +.slider-hero .video-modal .wrap { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + margin: auto; + padding: 5px; + background: rgba(255, 255, 255, 0.65); + border-radius: 5px; + width: 630px; + height: 360px; + opacity: 0; + filter: alpha(opacity=0); + -moz-transform: scale3d(0.3, 0.3, 0.3); + -o-transform: scale3d(0.3, 0.3, 0.3); + -ms-transform: scale3d(0.3, 0.3, 0.3); + -webkit-transform: scale3d(0.3, 0.3, 0.3); + transform: scale3d(0.3, 0.3, 0.3); + -webkit-transition: all 0.4s ease-out; + -moz-transition: all 0.4s ease-out; + -ms-transition: all 0.4s ease-out; + -o-transition: all 0.4s ease-out; + transition: all 0.4s ease-out; +} + +.features-hover-section { + margin-top: 120px; +} +@media (max-width: 991px) { + .features-hover-section { + margin-top: 60px; + } +} +.features-hover-section .images { + height: 355px; +} +@media (max-width: 767px) { + .features-hover-section .images { + height: 300px; + } +} +.features-hover-section .images img { + margin: 0 auto; + position: absolute; + left: 0; + right: 0; + display: none; +} +.features-hover-section .images img.active { + display: block; +} +.features-hover-section .features { + margin-top: 50px; +} +@media (max-width: 991px) { + .features-hover-section .features { + margin-top: 0px; + } +} +.features-hover-section .features .feature { + cursor: pointer; +} +@media (min-width: 992px) { + .features-hover-section .features .feature { + width: 85%; + margin: 0 auto; + } +} +.features-hover-section .features .feature.active strong, .features-hover-section .features .feature:hover strong { + color: #58B0D5; +} +.features-hover-section .features .feature.active p, .features-hover-section .features .feature:hover p { + color: #555; +} +.features-hover-section .features .feature strong { + color: #b6b6b6; + font-size: 16px; + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -ms-transition: color 0.2s linear; + -o-transition: color 0.2s linear; + transition: color 0.2s linear; +} +.features-hover-section .features .feature p { + margin-top: 15px; + line-height: 25px; + color: #b6b6b6; + -webkit-transition: color 0.2s linear; + -moz-transition: color 0.2s linear; + -ms-transition: color 0.2s linear; + -o-transition: color 0.2s linear; + transition: color 0.2s linear; +} + +.sidebar-toggle { + width: 30px; + float: right; + margin-top: 15px; + cursor: pointer; +} +.sidebar-toggle .line { + background: #fff; + width: 100%; + height: 3px; + margin-bottom: 6px; + border-radius: 1px; +} + +.st-container { + position: relative; + overflow: hidden; +} +.st-container--right-side { + /* show hidden navbar */ +} +.st-container--right-side .nav-menu { + right: 0; + -moz-transform: translate3d(50%, 0, 0); + -o-transform: translate3d(50%, 0, 0); + -ms-transform: translate3d(50%, 0, 0); + -webkit-transform: translate3d(50%, 0, 0); + transform: translate3d(50%, 0, 0); +} +.st-container--right-side.nav-effect .nav-menu { + -moz-transform: translate3d(0, 0, 0); + -o-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} +.st-container--right-side.nav-effect .st-pusher { + -moz-transform: translate3d(-320px, 0, 0); + -o-transform: translate3d(-320px, 0, 0); + -ms-transform: translate3d(-320px, 0, 0); + -webkit-transform: translate3d(-320px, 0, 0); + transform: translate3d(-320px, 0, 0); + box-shadow: 5px 0px 20px 0px rgba(0, 0, 0, 0.5); +} +@media (max-width: 767px) { + .st-container--right-side.nav-effect .st-pusher { + -moz-transform: translate3d(-220px, 0, 0); + -o-transform: translate3d(-220px, 0, 0); + -ms-transform: translate3d(-220px, 0, 0); + -webkit-transform: translate3d(-220px, 0, 0); + transform: translate3d(-220px, 0, 0); + } +} +.st-container--left-side { + /* show hidden navbar */ +} +.st-container--left-side .nav-menu { + left: 0; + -moz-transform: translate3d(-50%, 0, 0); + -o-transform: translate3d(-50%, 0, 0); + -ms-transform: translate3d(-50%, 0, 0); + -webkit-transform: translate3d(-50%, 0, 0); + transform: translate3d(-50%, 0, 0); +} +.st-container--left-side.nav-effect .nav-menu { + -moz-transform: translate3d(0, 0, 0); + -o-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} +.st-container--left-side.nav-effect .st-pusher { + -moz-transform: translate3d(300px, 0, 0); + -o-transform: translate3d(300px, 0, 0); + -ms-transform: translate3d(300px, 0, 0); + -webkit-transform: translate3d(300px, 0, 0); + transform: translate3d(300px, 0, 0); + box-shadow: -5px 0px 20px 0px rgba(0, 0, 0, 0.5); +} +@media (max-width: 767px) { + .st-container--left-side.nav-effect .st-pusher { + -moz-transform: translate3d(220px, 0, 0); + -o-transform: translate3d(220px, 0, 0); + -ms-transform: translate3d(220px, 0, 0); + -webkit-transform: translate3d(220px, 0, 0); + transform: translate3d(220px, 0, 0); + } +} + +.st-pusher { + position: relative; + left: 0; + z-index: 99; + background: #fff; + -webkit-transition: -webkit-transform 0.35s ease-out; + -moz-transition: -moz-transform 0.35s ease-out; + -ms-transition: -ms-transform 0.35s ease-out; + -o-transition: -o-transform 0.35s ease-out; + transition: transform 0.35s ease-out; +} + +.nav-menu { + position: fixed; + top: 0; + z-index: 1; + width: 320px; + height: 100%; + background: #2a313a; + padding-top: 30px; + -webkit-transition: -webkit-transform 0.35s ease-out; + -moz-transition: -moz-transform 0.35s ease-out; + -ms-transition: -ms-transform 0.35s ease-out; + -o-transition: -o-transform 0.35s ease-out; + transition: transform 0.35s ease-out; +} +@media (max-width: 767px) { + .nav-menu { + width: 220px; + position: absolute; + } +} +.nav-menu .main-menu h3 { + color: #fff; + font-size: 26px; + margin-left: 50px; + margin-bottom: 30px; +} +@media (max-width: 767px) { + .nav-menu .main-menu h3 { + margin-left: 30px; + font-size: 23px; + } +} +.nav-menu .main-menu a { + display: block; + padding: 5px 30px 5px 50px; + margin-bottom: 10px; + text-decoration: none; + color: #fff; + font-size: 15px; + -webkit-transition: all 0.15s linear; + -moz-transition: all 0.15s linear; + -ms-transition: all 0.15s linear; + -o-transition: all 0.15s linear; + transition: all 0.15s linear; +} +@media (max-width: 767px) { + .nav-menu .main-menu a { + padding-left: 30px; + font-size: 16px; + } +} +.nav-menu .main-menu a:hover { + color: #82C4F8; +} +.nav-menu .main-menu .social { + margin-left: 45px; + margin-top: 50px; +} +@media (max-width: 767px) { + .nav-menu .main-menu .social { + margin-left: 30px; + } +} +.nav-menu .main-menu .social a { + padding: 0px; + display: inline-block; + margin-right: 22px; +} +.nav-menu .main-menu .social a:hover i { + color: #7EBDE7; +} +.nav-menu .main-menu .social a i { + font-size: 22px; + color: #fff; + -webkit-transition: all 0.15s linear; + -moz-transition: all 0.15s linear; + -ms-transition: all 0.15s linear; + -o-transition: all 0.15s linear; + transition: all 0.15s linear; +} +.nav-menu .projects .back { + margin-top: 10px; + font-weight: 500; + font-size: 21px; + color: #fff; + margin-bottom: 20px; +} +.nav-menu .projects a { + display: block; + position: relative; + left: 0px; + padding: 15px 40px; + text-decoration: none; + color: #fff; + font-size: 16px; + font-weight: 400; + -webkit-transition: all 0.15s linear; + -moz-transition: all 0.15s linear; + -ms-transition: all 0.15s linear; + -o-transition: all 0.15s linear; + transition: all 0.15s linear; +} +@media (max-width: 767px) { + .nav-menu .projects a { + padding-left: 20px; + } +} +.nav-menu .projects a:hover { + left: 10px; +} +.nav-menu .projects a p { + color: #A2B1C2; + font-size: 14px; + margin-top: 5px; +} + +.index-sidebar-hero { + background: #3D4046; + height: 630px; + position: relative; + overflow: hidden; + box-shadow: 0px 1px 3px -1px rgba(0, 0, 0, 0.35); + padding-top: 130px; +} +@media (max-width: 991px) { + .index-sidebar-hero { + padding-top: 100px; + height: 550px; + } +} +.index-sidebar-hero #cover-image { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + padding-top: 105px; + background-position: center center; + background-size: cover; + -webkit-animation-delay: 0.15s; + -moz-animation-delay: 0.15s; + -o-animation-delay: 0.15s; + -ms-animation-delay: 0.15s; + animation-delay: 0.15s; + background-image: url("../../images/bg4.png"); +} +@media (max-width: 991px) { + .index-sidebar-hero #cover-image { + padding-top: 65px; + } +} +.index-sidebar-hero #cover-image:after { + content: ""; + position: absolute; + display: block; + height: 100%; + left: 0; + width: 100%; + top: 0; + background: rgba(24, 22, 19, 0.4); +} +.index-sidebar-hero #cover-image:before { + position: absolute; + top: 10%; + bottom: 0; + left: 0; + right: 0; + content: ""; + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(0, 0, 0, 0)), color-stop(100%, rgba(0, 0, 0, 0.25))); + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0.25) 100%); + background: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.25)); + background: -o-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.25)); + background: linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.25)); +} +.index-sidebar-hero .container { + position: relative; + z-index: 3; + height: 100%; +} +.index-sidebar-hero .hero-text { + text-align: center; + font-size: 35px; + font-weight: 400; + color: #fff; + margin-top: 50px; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.55); + -webkit-animation-delay: 0.6s; + -moz-animation-delay: 0.6s; + -o-animation-delay: 0.6s; + -ms-animation-delay: 0.6s; + animation-delay: 0.6s; +} +@media (max-width: 991px) { + .index-sidebar-hero .hero-text { + font-size: 30px; + } +} +@media (max-width: 767px) { + .index-sidebar-hero .hero-text { + font-size: 27px; + } +} +.index-sidebar-hero .cta { + text-align: center; + margin-top: 40px; + -webkit-animation-delay: 0.6s; + -moz-animation-delay: 0.6s; + -o-animation-delay: 0.6s; + -ms-animation-delay: 0.6s; + animation-delay: 0.6s; +} +.index-sidebar-hero .cta a { + margin: 0px 20px; +} +@media (max-width: 400px) { + .index-sidebar-hero .cta a { + display: block; + } +} +.index-sidebar-hero .cta a.button { + border-radius: 50px; + padding: 12px 36px; + font-size: 18px; + -webkit-transition: all 0.3s linear; + -moz-transition: all 0.3s linear; + -ms-transition: all 0.3s linear; + -o-transition: all 0.3s linear; + transition: all 0.3s linear; +} +@media (max-width: 767px) { + .index-sidebar-hero .cta a.button { + font-size: 16px; + } +} +@media (max-width: 400px) { + .index-sidebar-hero .cta a.button { + width: 65%; + margin: 0 auto; + margin-bottom: 25px; + } +} +.index-sidebar-hero .cta a.button:hover { + box-shadow: rgba(0, 0, 0, 0.3) 0 1px 2px, inset rgba(255, 255, 255, 0.88) 0px 1px 3px -1px, rgba(0, 0, 0, 0.3) 0 1px 2px, #69B1F3 0 1px 30px; +} +.index-sidebar-hero .cta a.demo { + color: #fff; + font-size: 17px; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3); +} +@media (max-width: 767px) { + .index-sidebar-hero .cta a.demo { + font-size: 16px; + } +} +.index-sidebar-hero .screenshot { + position: absolute; + bottom: 0; + left: 0; + right: 0; +} +.index-sidebar-hero .screenshot img { + margin: 0 auto; +} + +.cta-mobile-section { + background: #F7F8FB; + border: 1px solid #EAEDF7; + box-shadow: inset rgba(100, 100, 100, 0.25) 0 1px 1px; + margin-top: 100px; +} +@media (min-width: 992px) { + .cta-mobile-section .container { + width: 870px; + } +} +.cta-mobile-section .device { + margin: -30px 0px 0px 0px; + position: relative; + top: 2px; +} +.cta-mobile-section .info { + margin-top: 50px; +} +@media (max-width: 767px) { + .cta-mobile-section .info { + margin: 35px; + text-align: center; + } +} +.cta-mobile-section .info h3 { + font-weight: 400; + color: #616974; + font-size: 26px; +} +.cta-mobile-section .info p { + font-size: 15px; + margin-top: 20px; + color: #6a6e7b; + line-height: 23px; +} +.cta-mobile-section .info .app-store { + margin-top: 18px; + border: none; + display: inline-block; +} +.cta-mobile-section .info .app-store img { + max-width: 135px; +} + +.call-to-action-signup { + margin-top: 110px; + margin-bottom: -120px; + background: #fafafa; + padding: 60px 0 70px; + border-top: 1px solid #eee; + border-bottom: 1px solid #eee; +} +@media (min-width: 1200px) { + .call-to-action-signup .container { + width: 1170px; + } +} +.call-to-action-signup h3 { + margin: 0; + font-size: 20px; + letter-spacing: 2px; + text-align: center; + color: #4c4c4c; + text-transform: uppercase; + font-weight: 600; + text-shadow: 1px 1px #fff; +} +.call-to-action-signup form { + margin-top: 40px; + position: relative; + text-align: center; +} +.call-to-action-signup form .form-group { + margin-right: 7px; + width: 250px; +} +@media (max-width: 991px) { + .call-to-action-signup form .form-group { + width: 220px; + } +} +@media (max-width: 767px) { + .call-to-action-signup form .form-group { + width: 100%; + } +} +.call-to-action-signup form input { + box-shadow: 0 1px 1px #fff; + height: 42px; + width: 100% !important; + font-size: 15px; +} +.call-to-action-signup form input:focus { + box-shadow: none; +} +@media (max-width: 1199px) { + .call-to-action-signup form [type=submit] { + margin-top: 30px; + } +} +.call-to-action-signup form .agree { + top: 20px; + position: relative; + display: block; + text-align: right; + padding-right: 95px; + color: #555; +} +@media (max-width: 1199px) { + .call-to-action-signup form .agree { + text-align: center; + padding: 0; + } +} + +#demo .modal-dialog { + padding-top: 100px; + width: 650px; +} +#demo .modal-body { + padding: 0px; +} + +.features-tabs-section { + margin-top: 60px; +} +@media (max-width: 991px) { + .features-tabs-section { + margin-bottom: 20px; + } +} +.features-tabs-section .header { + text-align: center; +} +.features-tabs-section .header h3 { + font-size: 23px; + color: #55565d; + line-height: 25px; + font-weight: 500; +} +.features-tabs-section .header p { + font-size: 16px; + font-weight: 400; + color: #888; +} +.features-tabs-section .tabs-wrapper { + margin-top: 45px; +} +.features-tabs-section .tabs-wrapper .nav-tabs { + text-align: center; + border-bottom: 2px solid #EDEDED; +} +@media (max-width: 991px) { + .features-tabs-section .tabs-wrapper .nav-tabs { + display: none; + } +} +.features-tabs-section .tabs-wrapper .nav-tabs li { + float: none; + display: inline-block; +} +.features-tabs-section .tabs-wrapper .nav-tabs li.active a { + color: #2B69A2; + font-weight: 400; + border-bottom: 0px; +} +.features-tabs-section .tabs-wrapper .nav-tabs li.active a:after { + content: ""; + position: absolute; + width: 100%; + height: auto; + border-bottom: 2px solid #7BA3C9; + left: 1px; + bottom: -1px; +} +.features-tabs-section .tabs-wrapper .nav-tabs li a { + color: #888; + font-weight: 400; + border: 0px; + padding: 10px 20px; + font-size: 15px; + -webkit-transition: color 0.25s linear; + -moz-transition: color 0.25s linear; + -ms-transition: color 0.25s linear; + -o-transition: color 0.25s linear; + transition: color 0.25s linear; +} +.features-tabs-section .tabs-wrapper .nav-tabs li a:hover { + color: #2B69A2; + background: none; +} +.features-tabs-section .tabs-wrapper .tab-content { + margin-top: 60px; +} +.features-tabs-section .tabs-wrapper .tab-content .tab-pane { + height: 325px; +} +@media (max-width: 991px) { + .features-tabs-section .tabs-wrapper .tab-content .tab-pane { + display: block !important; + opacity: 1; + filter: alpha(opacity=100); + height: auto; + text-align: center; + margin-bottom: 70px; + } +} +@media (max-width: 991px) { + .features-tabs-section .tabs-wrapper .tab-content .tab-pane .info { + margin-bottom: 35px; + } +} +.features-tabs-section .tabs-wrapper .tab-content .tab-pane .info h4 { + color: #3B88B6; + font-weight: normal; + font-size: 19px; + line-height: 25px; + margin-top: 35px; +} +.features-tabs-section .tabs-wrapper .tab-content .tab-pane .info p { + color: #6C7279; + font-size: 15px; + margin-top: 20px; + line-height: 24px; + width: 83%; + font-weight: normal; +} +@media (max-width: 991px) { + .features-tabs-section .tabs-wrapper .tab-content .tab-pane .info p { + margin: 0 auto; + width: 95%; + } +} +.features-tabs-section .tabs-wrapper .tab-content .tab-pane .image img { + display: inline-block; + margin: 0 auto; +} + +.features-grid-section { + margin-top: 100px; +} +@media (max-width: 991px) { + .features-grid-section { + margin-top: 50px; + } +} +@media (min-width: 992px) { + .features-grid-section .row { + margin-bottom: 50px; + } +} +@media (max-width: 991px) { + .features-grid-section .feature { + text-align: center; + margin-bottom: 35px; + } +} +.features-grid-section .feature img { + max-width: 40px; +} +.features-grid-section .feature .icon { + font-size: 32px; + color: #4187ba; +} +.features-grid-section .feature strong { + display: block; + font-weight: 400; + font-size: 17px; + color: #354E75; + margin-top: 15px; +} +.features-grid-section .feature p { + font-weight: normal; + font-size: 14px; + color: #797979; + margin-top: 10px; + line-height: 24px; +} + +.blog-showcase { + margin-top: 120px; +} +.blog-showcase .header { + text-align: center; + margin-bottom: 45px; +} +.blog-showcase .header h3 { + font-size: 25px; + color: #444; + line-height: 25px; + font-weight: 400; +} +.blog-showcase .pics { + text-align: center; +} +.blog-showcase .pics .pic { + position: relative; + width: 250px; + height: 258px; + margin: 0 auto; + margin-left: 10px; + margin-right: 10px; + display: inline-block; + opacity: 1; + filter: alpha(opacity=100); + -webkit-transition: all 0.25s linear; + -moz-transition: all 0.25s linear; + -ms-transition: all 0.25s linear; + -o-transition: all 0.25s linear; + transition: all 0.25s linear; +} +@media (max-width: 991px) { + .blog-showcase .pics .pic { + margin-bottom: 35px; + } +} +.blog-showcase .pics .pic:hover { + opacity: 0.7; + filter: alpha(opacity=70); + text-decoration: none; +} +.blog-showcase .pics .pic .bg { + position: absolute; + padding: 80px 20px 20px; + bottom: 0; + left: 0; + right: 0; + border-radius: 0px 5px 5px 0px; + background-repeat: repeat-x; + background-image: -webkit-linear-gradient(bottom, rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.3) 30%, rgba(0, 0, 0, 0) 100%); + background-image: -moz-linear-gradient(bottom, rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.3) 30%, rgba(0, 0, 0, 0) 100%); + background-image: -ms-linear-gradient(bottom, rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.3) 30%, rgba(0, 0, 0, 0) 100%); + background-image: -o-linear-gradient(bottom, rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.3) 30%, rgba(0, 0, 0, 0) 100%); + background-image: linear-gradient(to top, rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.3) 30%, rgba(0, 0, 0, 0) 100%); +} +.blog-showcase .pics .pic img { + display: block; + margin: 0 auto; + border-radius: 5px; +} +.blog-showcase .pics .pic p { + text-align: left; + color: #fff; + font-size: 22px; + position: absolute; + line-height: 28px; + bottom: 7px; + left: 20px; + width: 75%; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.4); +} + +.services-intro { + margin-top: 90px; +} +@media (min-width: 992px) { + .services-intro .row { + border-bottom: 1px solid #E6E6E8; + } +} +@media (max-width: 991px) { + .services-intro .image img { + margin: 0 auto; + } +} +@media (min-width: 992px) { + .services-intro .info { + padding-left: 60px; + } +} +@media (max-width: 991px) { + .services-intro .info { + text-align: center; + } +} +.services-intro .info h3 { + font-weight: 400; + margin-top: 70px; + font-size: 28px; +} +.services-intro .info p { + color: #666; + margin-top: 25px; + line-height: 23px; +} +@media (min-width: 992px) { + .services-intro .info p { + width: 85%; + } +} + +.services-tabs { + margin-top: 130px; +} +@media (max-width: 991px) { + .services-tabs { + margin-top: 70px; + } +} +.services-tabs .hexagons { + text-align: center; +} +.services-tabs .hexagons .hexagon { + background: url("../../images/hexagon.png") 0 0 no-repeat; + width: 100px; + height: 113px; + position: relative; + display: inline-block; + margin-right: 60px; + cursor: pointer; +} +@media (max-width: 991px) { + .services-tabs .hexagons .hexagon { + margin: 0; + } +} +.services-tabs .hexagons .hexagon:hover, .services-tabs .hexagons .hexagon.active { + background-position: 0 -117px; +} +.services-tabs .hexagons .hexagon:hover .icon, .services-tabs .hexagons .hexagon.active .icon { + color: #fff; +} +.services-tabs .hexagons .hexagon:last-child { + margin-right: 0px; +} +.services-tabs .hexagons .hexagon .icon { + font-size: 48px; + color: #80939b; + position: absolute; + top: 32px; + left: 0; + right: 0; + text-align: center; +} +.services-tabs .sections { + margin-top: 35px; + position: relative; + height: 220px; +} +.services-tabs .sections .section { + display: none; + position: absolute; +} +.services-tabs .sections .section.active { + display: block; +} +.services-tabs .sections .section h4 { + margin-bottom: 18px; +} +.services-tabs .sections .section p { + line-height: 25px; +} + +.services-grid-section { + margin-top: 100px; +} +.services-grid-section.with-border { + margin-top: 70px; +} +.services-grid-section.with-border .header { + border-top: 1px solid #E6E6E8; + padding-top: 55px; +} +.services-grid-section .header { + text-align: center; +} +.services-grid-section .header h3 { + font-size: 26px; + color: #555C68; + line-height: 25px; + font-weight: 400; +} +.services-grid-section .header p { + font-size: 16px; + font-weight: 400; + color: #888; +} +.services-grid-section .sections { + margin-top: 45px; +} +.services-grid-section .sections .section { + text-align: center; +} +@media (max-width: 991px) { + .services-grid-section .sections .section { + margin-bottom: 40px; + } +} +.services-grid-section .sections .section .pic { + display: inline-block; + padding: 15px; + background: #f5f5f5; + box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.25), inset 0px 0px 1px 1px #fff; +} +.services-grid-section .sections .section .info { + margin-top: 25px; + text-align: center; +} +@media (min-width: 992px) { + .services-grid-section .sections .section .info { + margin-left: 45px; + width: 80%; + text-align: left; + } +} +.services-grid-section .sections .section .info strong { + font-weight: 500; + font-size: 18px; + color: #555; +} +.services-grid-section .sections .section .info p { + color: #666; + margin-top: 15px; + line-height: 22px; +} + +.services-showcase-section { + margin-top: 80px; +} +@media (max-width: 991px) { + .services-showcase-section { + margin-top: 50px; + } +} +.services-showcase-section .header { + border-top: 1px solid #E6E6E8; + padding-top: 55px; + margin-bottom: 35px; +} +.services-showcase-section .header h3 { + font-size: 27px; + color: #444; + line-height: 25px; + font-weight: 500; + margin-bottom: 7px; +} +@media (max-width: 991px) { + .services-showcase-section .header h3 { + text-align: center; + } +} +.services-showcase-section .header p { + font-size: 15px; + font-weight: 400; + color: #666; + width: 50%; + line-height: 24px; + margin-top: 15px; +} +@media (max-width: 991px) { + .services-showcase-section .header p { + margin: 0 auto; + width: 90%; + text-align: center; + } +} +.services-showcase-section .features { + margin-bottom: 20px; +} +@media (max-width: 991px) { + .services-showcase-section .features .feature { + text-align: center; + margin-bottom: 30px; + } +} +.services-showcase-section .features .feature img { + max-width: 40px; + position: relative; + top: 10px; +} +@media (min-width: 992px) { + .services-showcase-section .features .feature img { + float: left; + } +} +@media (min-width: 992px) { + .services-showcase-section .features .feature .info { + float: left; + width: 80%; + margin-left: 20px; + } +} +.services-showcase-section .features .feature .info strong { + display: block; + font-weight: 500; + font-size: 16px; + color: #444; + margin-top: 15px; +} +.services-showcase-section .features .feature .info p { + font-weight: normal; + font-size: 14px; + color: #797979; + margin-top: 10px; + line-height: 24px; +} + +.signup-cta { + margin-top: 120px; +} +.signup-cta .wrapper { + background: #F7F8FB; + border: 1px solid #EAEDF7; + border-radius: 8px; + padding: 30px 60px; + box-shadow: inset rgba(100, 100, 100, 0.25) 0 1px 1px; + margin: 0 auto; +} +@media (max-width: 991px) { + .signup-cta .wrapper { + text-align: center; + } +} +.signup-cta h4 { + font-size: 20px; + color: #5F6E7C; + font-weight: 400; + float: left; + text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.8); +} +@media (max-width: 767px) { + .signup-cta h4 { + font-size: 17px; + line-height: 23px; + } +} +.signup-cta .button { + float: right; +} +@media (max-width: 991px) { + .signup-cta .button { + float: none; + margin-top: 20px; + } +} + +.session-page { + min-height: 100vh; + background-attachment: fixed !important; + background: #5E7A9B; + background: -webkit-linear-gradient(top, #355069 0, #5E7A9B 100%); + background: -moz-linear-gradient(top, #355069 0, #5E7A9B 100%); + background: -o-linear-gradient(top, #355069 0, #5E7A9B 100%); + background: linear-gradient(top, #355069 0, #5E7A9B 100%); +} +.session-page .header .logo { + margin-top: 40px; + text-align: center; +} +.session-page .header .logo a { + color: #fff; + font-size: 31px; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.6); + -webkit-transition: color 0.25s linear; + -moz-transition: color 0.25s linear; + -ms-transition: color 0.25s linear; + -o-transition: color 0.25s linear; + transition: color 0.25s linear; +} +.session-page .header .logo a:hover { + text-decoration: none; + color: #bbb; +} +.session-page .header h4 { + color: #fff; + text-align: center; + font-size: 25px; + font-weight: 400; + margin-top: 45px; + margin-bottom: 5px; +} +.session-page .header p { + color: #fff; + text-align: center; +} +.session-page .wrapper { + margin: 0 auto; + width: 51%; + margin-top: 25px; + background: #fff; + padding: 15px 50px 25px 50px; + border-radius: 5px; + border-top: 6px solid #6ED5E4; +} +@media (max-width: 991px) { + .session-page .wrapper { + width: 67%; + } +} +@media (max-width: 767px) { + .session-page .wrapper { + width: 100%; + } +} +.session-page .wrapper .formy { + margin-top: 22px; +} +.session-page .wrapper .formy .form-group label { + color: #46515F; +} +.session-page .wrapper .formy .checkbox label { + color: #666; + font-size: 13px; +} +.session-page .wrapper .formy .checkbox input { + position: relative; + top: -1px; +} +.session-page .wrapper .formy .submit { + text-align: center; +} +.session-page .wrapper .formy .submit .button-clear { + margin-top: 35px; + display: inline-block; +} +.session-page .wrapper .formy .info { + float: right; + width: 67%; +} +.session-page .wrapper .formy .info .good-company strong { + font-size: 15px; + font-weight: 500; +} +.session-page .wrapper .formy .info .good-company p { + color: #858585; + margin-top: 9px; +} +.session-page .wrapper .formy .info .testimonial { + margin-top: 40px; +} +.session-page .wrapper .formy .info .testimonial .quote { + font-style: italic; + font-size: 13px; + color: #555; + line-height: 20px; +} +.session-page .wrapper .formy .info .testimonial .author { + margin-top: 20px; +} +.session-page .wrapper .formy .info .testimonial .author .pic { + width: 55px; + height: 55px; + border-radius: 50px; + float: left; + position: relative; + top: -6px; + margin-right: 18px; +} +.session-page .wrapper .formy .info .testimonial .author .name { + color: #3C92C5; + line-height: 23px; + font-weight: 500; +} +.session-page .wrapper .formy .info .testimonial .author .company { + font-size: 14px; + color: #909090; + line-height: 23px; + font-weight: 400; +} +.session-page .already-account { + text-align: center; + color: #fff; + margin-top: 25px; + margin-bottom: 70px; + font-size: 13px; + font-weight: 500; +} +.session-page .already-account a { + color: #fff; + text-decoration: underline; +} +.session-page .already-account .popover-content { + color: #000; + font-size: 17px; + font-weight: 400; +} + +.signup-rotate { + min-height: 100vh; + background-attachment: fixed !important; + background: #5E7A9B; + background: -webkit-linear-gradient(top, #355069 0, #5E7A9B 100%); + background: -moz-linear-gradient(top, #355069 0, #5E7A9B 100%); + background: -o-linear-gradient(top, #355069 0, #5E7A9B 100%); + background: linear-gradient(top, #355069 0, #5E7A9B 100%); +} +.signup-rotate .header .logo { + margin-top: 40px; + text-align: center; +} +.signup-rotate .header .logo a { + color: #fff; + font-size: 31px; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.6); + -webkit-transition: color 0.25s linear; + -moz-transition: color 0.25s linear; + -ms-transition: color 0.25s linear; + -o-transition: color 0.25s linear; + transition: color 0.25s linear; +} +.signup-rotate .header .logo a:hover { + text-decoration: none; + color: #bbb; +} +.signup-rotate .header h4 { + color: #fff; + text-align: center; + font-size: 25px; + font-weight: 400; + margin-top: 45px; + margin-bottom: 5px; +} +.signup-rotate .header p { + color: #fff; + text-align: center; +} +.signup-rotate .rotate-container { + -webkit-perspective: 1000; + -moz-perspective: 1000; + -ms-perspective: 1000; + -o-perspective: 1000; + margin: 0 auto; +} +.signup-rotate .wrappers { + min-height: 445px; + -webkit-transition: -webkit-transform 1s; + -moz-transition: -moz-transform 1s; + -ms-transition: -ms-transform 1s; + -o-transition: -o-transform 1s; + transition: transform 1s; + -webkit-transform-style: preserve-3d; + -moz-transform-style: preserve-3d; + -ms-transform-style: preserve-3d; + -o-transform-style: preserve-3d; + transform-style: preserve-3d; +} +.signup-rotate .wrappers.flipped { + -moz-transform: rotateY(180deg); + -o-transform: rotateY(180deg); + -ms-transform: rotateY(180deg); + -webkit-transform: rotateY(180deg); + transform: rotateY(180deg); +} +.signup-rotate .wrappers.flipped-ended .front { + z-index: 1; +} +.signup-rotate .wrappers.flipped-ended .back { + z-index: 3; +} +.signup-rotate .wrapper { + margin: 0 auto; + width: 51%; + margin-top: 25px; + background: #fff; + padding: 15px 50px 25px 50px; + border-radius: 5px; + border-top: 6px solid #6ED5E4; + position: absolute; + left: 0; + right: 0; + -webkit-backface-visibility: hidden; + -moz-backface-visibility: hidden; + -o-backface-visibility: hidden; + /* These are commented out because they break the rotation on IE */ + /*-ms-backface-visibility: hidden;*/ + /*backface-visibility: hidden;*/ + -webkit-transform-style: preserve-3d; + -moz-transform-style: preserve-3d; + -ms-transform-style: preserve-3d; + -o-transform-style: preserve-3d; + transform-style: preserve-3d; +} +@media (max-width: 991px) { + .signup-rotate .wrapper { + width: 67%; + } +} +@media (max-width: 767px) { + .signup-rotate .wrapper { + width: 100%; + } +} +.signup-rotate .wrapper.front { + z-index: 3; +} +.signup-rotate .wrapper.back { + z-index: 1; + -moz-transform: rotateY(180deg); + -o-transform: rotateY(180deg); + -ms-transform: rotateY(180deg); + -webkit-transform: rotateY(180deg); + transform: rotateY(180deg); +} +.signup-rotate .wrapper.back h3 { + margin-top: -5px; + margin-bottom: 45px; + text-align: center; + font-weight: 400; +} +.signup-rotate .wrapper .formy { + margin-top: 22px; +} +.signup-rotate .wrapper .formy .form-group label { + color: #46515F; +} +.signup-rotate .wrapper .formy .checkbox label { + color: #666; + font-size: 13px; +} +.signup-rotate .wrapper .formy .checkbox input { + position: relative; + top: -1px; +} +.signup-rotate .wrapper .formy .submit { + text-align: center; +} +.signup-rotate .wrapper .formy .submit .button-clear { + margin-top: 35px; + display: inline-block; +} +.signup-rotate .change-view { + text-align: center; + color: #fff; + margin-top: 25px; + margin-bottom: 70px; + font-size: 13px; + font-weight: 500; + display: none; +} +.signup-rotate .change-view.active { + display: block; +} +.signup-rotate .change-view a { + color: #fff; + text-decoration: underline; +} +.signup-rotate .change-view .popover-content { + color: #000; + font-size: 17px; + font-weight: 400; +} + +.blog-posts { + margin-top: 60px; +} +.blog-posts .post { + margin-bottom: 90px; +} +.blog-posts .post .pic { + border: 0px; +} +.blog-posts .post .pic:hover img { + opacity: 0.9; + filter: alpha(opacity=90); +} +.blog-posts .post .pic img { + opacity: 1; + filter: alpha(opacity=100); + -webkit-transition: all 0.25s linear; + -moz-transition: all 0.25s linear; + -ms-transition: all 0.25s linear; + -o-transition: all 0.25s linear; + transition: all 0.25s linear; +} +@media (max-width: 991px) { + .blog-posts .post .pic img { + margin: 0 auto; + } +} +.blog-posts .post .video { + /* make video fluid for responsiveness */ +} +@media (min-width: 992px) { + .blog-posts .post .video { + max-width: 635px; + } +} +.blog-posts .post .video .iframe-wrapper { + position: relative; + padding-bottom: 56%; + height: 0; +} +.blog-posts .post .video .iframe-wrapper iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.blog-posts .post .title { + font-family: Myriad Pro, Lato, Helvetica Neue, Arial; + font-size: 28px; + font-weight: 600; + margin-top: 33px; +} +@media (max-width: 991px) { + .blog-posts .post .title { + text-align: center; + margin-bottom: 20px; + } +} +@media (max-width: 767px) { + .blog-posts .post .title { + font-size: 23px; + } +} +.blog-posts .post .title a { + color: #111; + -webkit-transition: color 0.15s linear; + -moz-transition: color 0.15s linear; + -ms-transition: color 0.15s linear; + -o-transition: color 0.15s linear; + transition: color 0.15s linear; +} +.blog-posts .post .title a:hover { + text-decoration: none; + color: #428bca; +} +.blog-posts .post .author { + color: #888; + margin-top: 4px; + position: relative; +} +.blog-posts .post .author .avatar { + max-width: 60px; + border-radius: 50px; + position: absolute; + left: -75px; + top: -15px; + border: 1px solid #e0e0e0; +} +.blog-posts .post .intro { + font-family: Source Sans Pro, Helvetica Neue, Arial; + margin-top: 18px; + font-size: 16px; + line-height: 27px; + color: #4d4d4d; +} +@media (min-width: 992px) { + .blog-posts .post .intro { + width: 90%; + } +} +.blog-posts .post .continue-reading { + font-family: Source Sans Pro, Helvetica Neue, Arial; + font-size: 15px; + display: inline-block; + margin-top: 8px; + -webkit-transition: color 0.25s linear; + -moz-transition: color 0.25s linear; + -ms-transition: color 0.25s linear; + -o-transition: color 0.25s linear; + transition: color 0.25s linear; +} +@media (max-width: 991px) { + .blog-posts .pages { + text-align: center; + } +} +@media (max-width: 991px) { + .blog-posts .sidebar { + margin-top: 70px; + text-align: center; + } +} +.blog-posts .sidebar .search { + margin-bottom: 40px; +} +.blog-posts .sidebar .search form { + position: relative; + left: -20px; +} +.blog-posts .sidebar .search .icomoon-search { + font-size: 16px; + color: #B9B9B9; + position: relative; + left: 25px; + top: 1px; + -webkit-transition: all 0.25s linear; + -moz-transition: all 0.25s linear; + -ms-transition: all 0.25s linear; + -o-transition: all 0.25s linear; + transition: all 0.25s linear; +} +.blog-posts .sidebar .search .icomoon-search.active { + color: #555; +} +.blog-posts .sidebar .search input { + border: 0px; + border-bottom: 1px solid #DFDFDF; + padding-left: 30px; + padding-bottom: 6px; + -webkit-transition: all 0.25s linear; + -moz-transition: all 0.25s linear; + -ms-transition: all 0.25s linear; + -o-transition: all 0.25s linear; + transition: all 0.25s linear; + color: #777; + font-weight: 300; +} +@media (max-width: 991px) { + .blog-posts .sidebar .search input { + width: 95%; + } +} +.blog-posts .sidebar .search input:focus { + border-bottom-color: #888; +} +.blog-posts .sidebar .search input::-webkit-input-placeholder { + color: #a5a5a5; + font-weight: 300; + font-size: 13px; +} +.blog-posts .sidebar .search input:-moz-placeholder { + color: #a5a5a5; + font-weight: 300; + font-size: 13px; +} +.blog-posts .sidebar .search input::-moz-placeholder { + color: #a5a5a5; + font-weight: 300; + font-size: 13px; +} +.blog-posts .sidebar .search input:-ms-input-placeholder { + color: #a5a5a5; + font-weight: 300; + font-size: 13px; +} +.blog-posts .sidebar .updates { + padding: 17px; + box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.2); + border-radius: 5px; +} +@media (min-width: 992px) { + .blog-posts .sidebar .updates { + width: 85%; + } +} +.blog-posts .sidebar .updates strong { + color: #444; +} +.blog-posts .sidebar .updates .fa-rss { + color: #428bca; + font-size: 16px; + position: relative; + margin-left: 5px; +} +.blog-posts .sidebar .updates p { + font-size: 13px; + margin-top: 9px; + color: #777; + margin-bottom: 5px; +} +.blog-posts .sidebar .follow-tw { + margin-top: 20px; +} +.blog-posts .sidebar .follow-tw img { + cursor: pointer; +} +.blog-posts .sidebar .best-hits { + margin-top: 35px; + padding-left: 5px; +} +@media (max-width: 991px) { + .blog-posts .sidebar .best-hits { + margin-top: 40px; + } +} +.blog-posts .sidebar .best-hits strong { + font-weight: 500; + color: #444; + margin-bottom: 15px; + font-size: 15px; + display: block; +} +.blog-posts .sidebar .best-hits a { + font-size: 13px; + display: inline-block; + margin-bottom: 5px; +} +@media (max-width: 991px) { + .blog-posts .sidebar .best-hits a { + display: block; + margin-bottom: 10px; + } +} + +.blog-cols-page { + background: #FCFCFC; +} + +.blog-cols-header { + margin-top: 60px; + position: relative; +} +.blog-cols-header h1 { + font-size: 23px; + margin-top: 0; + color: #555; +} +.blog-cols-header p { + color: #757575; + font-size: 17px; + margin-top: 12px; +} + +.blog-cols-wrapper { + margin-top: 40px; +} +@media (max-width: 767px) { + .blog-cols-wrapper { + text-align: center; + } +} +.blog-cols-wrapper .card { + display: inline-block; +} +@media (min-width: 992px) { + .blog-cols-wrapper .card:nth-child(3n+3) .card-wrapper { + margin: 20px 0; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .blog-cols-wrapper .card:nth-child(2n+2) .card-wrapper { + margin: 20px 0; + } +} +.blog-cols-wrapper .card .card-wrapper { + display: inline-block; + position: relative; + width: 295px; + background-color: #fff; + vertical-align: top; + text-align: left; + color: #4B4F56; + height: 480px; + margin: 20px 20px 20px 0; + box-shadow: 0 20px 20px rgba(0, 0, 0, 0.08); + -webkit-transition: all 250ms cubic-bezier(0.02, 0.01, 0.47, 1); + -moz-transition: all 250ms cubic-bezier(0.02, 0.01, 0.47, 1); + -ms-transition: all 250ms cubic-bezier(0.02, 0.01, 0.47, 1); + -o-transition: all 250ms cubic-bezier(0.02, 0.01, 0.47, 1); + transition: all 250ms cubic-bezier(0.02, 0.01, 0.47, 1); +} +@media (max-width: 991px) { + .blog-cols-wrapper .card .card-wrapper { + width: 340px; + } +} +@media (max-width: 767px) { + .blog-cols-wrapper .card .card-wrapper { + margin: 20px 0; + width: 290px; + } +} +.blog-cols-wrapper .card .card-wrapper:hover { + box-shadow: 0 40px 40px rgba(0, 0, 0, 0.16); + transform: translate(0, -20px); +} +.blog-cols-wrapper .card .card-bg { + background-size: cover; + background-position: center center; + background-repeat: no-repeat; + width: 100%; + height: 295px; +} +@media (max-width: 1199px) { + .blog-cols-wrapper .card .card-bg { + height: 260px; + } +} +.blog-cols-wrapper .card .card-intro { + padding: 20px 20px 0; +} +.blog-cols-wrapper .card .card-title { + font-size: 22px; + line-height: 32px; +} +.blog-cols-wrapper .card .card-min-read { + margin-top: 5px; + font-size: 12px; + color: #777; +} +.blog-cols-wrapper .card .card-description { + font-size: 15px; + margin-top: 13px; + line-height: 23px; + color: #90949C; +} + +.blog-pager { + margin-top: 80px; +} +.blog-pager li > a { + padding: 9px 20px; + box-shadow: 0 1px 0px 0 rgba(0, 0, 0, 0.15); + border-radius: 30px; + margin: 0 10px; + font-weight: 500; + font-size: 15px; + color: #5F8AB0; + display: inline-block; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.blog-pager li > a:hover { + background-color: #4a556b; + color: #fff; + box-shadow: none; +} + +.timeline-page { + background: #f7f8fa; +} + +.blog-timeline-header { + margin-top: 55px; + position: relative; +} +.blog-timeline-header h1 { + text-align: center; + text-transform: uppercase; + font-weight: 500; + font-size: 24px; + margin-top: 0; + letter-spacing: 1px; +} +.blog-timeline-header h1 a { + color: #222; + text-decoration: none; +} +.blog-timeline-header .slogan { + text-align: center; + color: #777; + font-size: 16px; +} + +.blog-timeline-wrapper { + margin-top: 40px; + position: relative; +} +.blog-timeline-wrapper:before { + content: ""; + position: absolute; + background: #e7e8ec; + width: 5px; + height: 96%; + left: 0; + right: 0; + top: 95px; + margin: 0 auto; + z-index: -1; +} +.blog-timeline-wrapper .post { + position: relative; + float: right; + clear: right; + width: 44%; + margin: 40px 0; + border-radius: 5px; +} +@media (max-width: 767px) { + .blog-timeline-wrapper .post { + width: 100%; + } +} +.blog-timeline-wrapper .post:before { + content: ""; + position: absolute; + width: 70px; + height: 4px; + background: #E7E8EC; + left: -60px; + top: 62px; +} +@media (max-width: 991px) { + .blog-timeline-wrapper .post:before { + left: -42px; + width: 43px; + } +} +@media (max-width: 767px) { + .blog-timeline-wrapper .post:before { + display: none; + } +} +.blog-timeline-wrapper .post:nth-child(2) { + margin-top: 100px; +} +.blog-timeline-wrapper .post.left { + float: left; + clear: left; +} +.blog-timeline-wrapper .post.left:before { + right: -51px; + left: inherit; +} +@media (max-width: 991px) { + .blog-timeline-wrapper .post.left:before { + right: -42px; + width: 43px; + } +} +.blog-timeline-wrapper .post.left .marker { + left: inherit; + right: -65px; +} +@media (max-width: 991px) { + .blog-timeline-wrapper .post.left .marker { + right: -52px; + } +} +.blog-timeline-wrapper .post .marker { + position: absolute; + width: 17px; + height: 17px; + border-radius: 25px; + background: #e7e8ec; + left: -65px; + top: 55px; + border: 3px solid #F7F8FA; +} +@media (max-width: 991px) { + .blog-timeline-wrapper .post .marker { + left: -51px; + } +} +@media (max-width: 767px) { + .blog-timeline-wrapper .post .marker { + display: none; + } +} +.blog-timeline-wrapper .post .entry { + position: relative; + padding: 20px; + height: 400px; + background-size: cover; + background-position: center center; + border-radius: 5px; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.26); +} +.blog-timeline-wrapper .post .entry:before { + position: absolute; + top: 50%; + bottom: 0; + left: 0; + right: 0; + content: ""; + border-radius: 0 0 5px 5px; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.94) 100%); + background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.94) 100%); + background: -o-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.94) 100%); + background: linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.94) 100%); +} +.blog-timeline-wrapper .post .entry.smallish { + height: 320px; +} +.blog-timeline-wrapper .post .entry.smallish .intro { + top: 68%; +} +.blog-timeline-wrapper .post .entry .intro { + position: relative; + top: 75%; + color: #FFF; + z-index: 9; + letter-spacing: 0.3px; +} +.blog-timeline-wrapper .post .entry .intro h4 { + font-size: 19px; + font-weight: 600; + text-transform: uppercase; + text-shadow: 1px 1px rgba(0, 0, 0, 0.4); + letter-spacing: 0.5px; +} +@media (max-width: 767px) { + .blog-timeline-wrapper .post .entry .intro h4 { + font-size: 17px; + } +} +@media (max-width: 767px) { + .blog-timeline-wrapper .post .entry .intro p { + font-size: 13px; + } +} +.blog-timeline-wrapper .post .news { + position: relative; + background-size: cover; + background-position: center center; + border-radius: 5px; + box-shadow: 0 1px 5px rgba(12, 26, 36, 0.25); + background: #fff; +} +.blog-timeline-wrapper .post .news .author { + position: absolute; + left: 0; + top: -35px; + right: 0; + text-align: center; +} +.blog-timeline-wrapper .post .news .author img { + border-radius: 50%; + width: 68px; +} +.blog-timeline-wrapper .post .news section { + padding: 60px 25px 75px 25px; +} +.blog-timeline-wrapper .post .news section h3 { + margin-top: 0; +} +.blog-timeline-wrapper .post .news section h3 a { + color: #333; + text-decoration: none; + font-size: 21px; + text-align: center; + display: block; + line-height: 31px; + -webkit-transition: all 0.15s; + -moz-transition: all 0.15s; + -ms-transition: all 0.15s; + -o-transition: all 0.15s; + transition: all 0.15s; +} +.blog-timeline-wrapper .post .news section h3 a:hover { + color: #459CE7; +} +.blog-timeline-wrapper .post .news section p { + margin-top: 21px; + line-height: 24px; + color: #555; +} +.blog-timeline-wrapper .post .news section a.read-more { + position: relative; + text-decoration: none; + display: inline-block; + margin-top: 10px; + font-size: 15px; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; +} +.blog-timeline-wrapper .post .news section a.read-more:hover:after { + right: -18px; +} +.blog-timeline-wrapper .post .news section a.read-more:after { + content: "▸"; + position: absolute; + right: -15px; + top: 0; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; +} +.blog-timeline-wrapper .post .news footer { + position: absolute; + bottom: 0; + padding: 12px 25px; + width: 100%; + font-weight: 500; + background: #EEF1F4; + color: #92A2B2; +} +.blog-timeline-wrapper .post .news footer .tag { + position: relative; + padding-left: 27px; + text-transform: uppercase; + font-weight: 600; + letter-spacing: 1px; + font-size: 11px; + top: 3px; +} +.blog-timeline-wrapper .post .news footer .tag span { + font-size: 24px; + position: absolute; + top: -7px; + left: 0; +} +.blog-timeline-wrapper .post .news footer .date { + font-weight: 600; + font-size: 14px; +} +.blog-timeline-wrapper .pager { + text-align: center; + margin-top: 50px; + margin-bottom: 0; +} +.blog-timeline-wrapper .pager a { + display: inline-block; + position: relative; + top: -10px; + background: #E7E8EC; + text-decoration: none; + text-transform: uppercase; + font-weight: 600; + letter-spacing: 1px; + padding: 6px 23px; + border-radius: 3px; + color: #444; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; +} +.blog-timeline-wrapper .pager a:hover { + box-shadow: 0 6px 15px -1px rgba(0, 0, 0, 0.15); +} + +.blog-post-hero { + height: 500px; + background-image: url("../../images/photo-1470753937643-efeb931202a9.jpeg"); + background-size: cover; + background-position: center center; + position: relative; + top: -20px; +} +@media (max-width: 991px) { + .blog-post-hero { + height: 400px; + } +} +.blog-post-hero:after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + background: rgba(64, 64, 64, 0.5); +} +.blog-post-hero .container { + position: relative; + z-index: 1; + text-align: center; +} +.blog-post-hero h1 { + width: 60%; + margin: 190px auto 0; + color: #fff; + font-family: "Karla", "Lato", "Helvetica Neue", Arial; + font-size: 38px; + line-height: 46px; + text-shadow: 1px 1px rgba(0, 0, 0, 0.2); +} +@media (max-width: 991px) { + .blog-post-hero h1 { + margin-top: 150px; + font-size: 35px; + line-height: 40px; + width: 70%; + } +} +@media (max-width: 767px) { + .blog-post-hero h1 { + width: 100%; + font-size: 28px; + line-height: 35px; + } +} + +.blog-post-author { + color: #888; + display: inline-block; +} +.blog-post-author img { + border-radius: 100%; + width: 55px; + margin-right: 17px; + position: relative; + top: -2px; +} +.blog-post-author span { + color: #333; +} + +.blog-post-date { + display: inline-block; + margin-left: 35px; + color: #888; +} +@media (max-width: 767px) { + .blog-post-date { + margin-left: 15px; + } +} +.blog-post-date span { + color: #333; +} + +.blog-post-share { + float: right; + color: #888; +} +@media (max-width: 767px) { + .blog-post-share { + float: none; + } +} +.blog-post-share a { + margin-left: 13px; + position: relative; + top: 3px; + text-decoration: none; +} +.blog-post-share i { + font-size: 25px; +} +.blog-post-share i.ion-social-twitter { + color: #1da1f2; +} +.blog-post-share i.ion-social-facebook { + color: #3b5998; + font-size: 28px; + margin-right: 3px; +} +.blog-post-share i.ion-social-buffer { + color: #2d2d2d; +} + +.blog-post-wrapper { + margin-top: 30px; +} +.blog-post-wrapper .post { + float: none; + margin: 0 auto; + margin-bottom: 50px; +} +.blog-post-wrapper .post .title { + font-size: 31px; + font-weight: 600; + margin-top: 40px; + color: #252525; +} +@media (max-width: 767px) { + .blog-post-wrapper .post .title { + font-size: 26px; + line-height: 28px; + } +} +.blog-post-wrapper .post .content { + margin-top: 45px; + font-size: 15px; + line-height: 30px; + color: #454545; + margin-bottom: 25px; +} +.blog-post-wrapper .post .content p { + margin-top: 28px; +} +.blog-post-wrapper .post .content blockquote { + border-left: 2px solid #c0c9dc; + color: rgba(51, 51, 51, 0.55); + margin: 35px 0; + padding: 5px 20px; + padding-left: 23px; +} +.blog-post-wrapper .post .content blockquote p { + margin: 0; + line-height: 30px; + font-weight: normal; +} +.blog-post-wrapper .post .content blockquote .quote-author { + margin-top: 14px; +} +.blog-post-wrapper .post .content img { + margin: 0 auto; + margin-top: 55px; + margin-bottom: 55px; +} +.blog-post-wrapper .post .content .divider { + border-bottom: 1px solid #DEDEDC; + width: 20%; + margin: 0 auto; + margin-top: 35px; + margin-bottom: 35px; +} +.blog-post-wrapper .post .share { + margin-top: 50px; +} +.blog-post-wrapper .post .other-posts { + margin-top: 50px; +} + +.pricing-3-options-section { + margin-top: 70px; +} +.pricing-3-options-section .header { + text-align: center; +} +.pricing-3-options-section .header h3 { + font-size: 24px; + color: #444; + line-height: 25px; + font-weight: 500; +} +.pricing-3-options-section .header p { + font-size: 16px; + font-weight: 400; + color: #888; +} +.pricing-3-options-section .charts { + margin-top: 45px; +} +.pricing-3-options-section .charts .chart { + background-color: #fff; + border: 1px solid #D8D8D8; + border-radius: 5px; + box-shadow: 0px 0px 2px 0px rgba(181, 181, 181, 0.3); + padding: 20px 40px; + position: relative; + text-align: center; + width: 97%; + min-height: 315px; + position: relative; + top: 20px; +} +@media (min-width: 992px) { + .pricing-3-options-section .charts .chart.first { + float: right; + left: 45px; + } + .pricing-3-options-section .charts .chart.last { + left: -45px; + } +} +@media (max-width: 991px) { + .pricing-3-options-section .charts .chart { + float: none !important; + left: 0px !important; + top: 0px !important; + margin: 0 auto; + width: 55%; + margin-bottom: 25px !important; + } + .pricing-3-options-section .charts .chart.last { + margin-bottom: 0px !important; + } +} +@media (max-width: 600px) { + .pricing-3-options-section .charts .chart { + width: 90% !important; + } +} +.pricing-3-options-section .charts .chart.featured { + z-index: 999; +} +@media (min-width: 992px) { + .pricing-3-options-section .charts .chart.featured { + margin: 0 auto; + top: 0px; + min-height: 445px; + } + .pricing-3-options-section .charts .chart.featured .btn-signup { + margin-top: 32px; + } +} +.pricing-3-options-section .charts .chart.featured .popular { + position: absolute; + top: 0px; + left: 0px; +} +.pricing-3-options-section .charts .chart.featured .quantity { + margin-top: 15px; + padding-bottom: 5px; +} +.pricing-3-options-section .charts .chart .plan-name { + text-align: center; + font-size: 20px; + font-weight: 400; + color: #444; + width: 90%; + margin: 0 auto; + margin-top: 5px; +} +.pricing-3-options-section .charts .chart .quantity { + text-align: center; + border-bottom: 1px solid #d5d5d5; + margin-top: 10px; +} +.pricing-3-options-section .charts .chart .quantity .dollar { + font-size: 19px; + position: relative; + top: -18px; +} +.pricing-3-options-section .charts .chart .quantity .price { + font-size: 49px; +} +.pricing-3-options-section .charts .chart .quantity .period { + font-size: 17px; + position: relative; + top: -8px; + margin-left: 4px; +} +.pricing-3-options-section .charts .chart .specs { + margin-top: 20px; +} +.pricing-3-options-section .charts .chart .specs .spec { + font-size: 15px; + color: #474747; + text-align: center; + font-weight: 300; + margin-bottom: 13px; +} +.pricing-3-options-section .charts .chart .specs .spec .variable { + color: #1FBEE7; + font-weight: 500; +} +.pricing-3-options-section .charts .chart .btn-signup { + margin-top: 20px; + height: 38px; +} +.pricing-3-options-section .message p { + text-align: center; + font-size: 13px; + color: #656565; + margin-top: 23px; +} +.pricing-3-options-section .faq { + margin-top: 40px; +} +.pricing-3-options-section .faq .header h3 { + font-size: 20px; + color: #454545; + text-align: left; +} +.pricing-3-options-section .faq .questions { + margin-top: 25px; +} +.pricing-3-options-section .faq .questions .question { + margin-bottom: 40px; +} +@media (min-width: 992px) { + .pricing-3-options-section .faq .questions .question { + width: 90%; + } +} +.pricing-3-options-section .faq .questions .question strong { + font-weight: 500; + font-size: 16px; + color: #454545; +} +.pricing-3-options-section .faq .questions .question p { + color: #555; + margin-top: 10px; + font-size: 13px; + line-height: 22px; +} +.pricing-3-options-section .contact { + margin-top: 40px; +} +.pricing-3-options-section .contact .wrapper { + background: #F7F8FB; + border: 1px solid #EAEDF7; + border-radius: 8px; + padding: 25px 60px; + box-shadow: inset rgba(100, 100, 100, 0.25) 0 1px 1px; + margin: 0 auto; +} +.pricing-3-options-section .contact h4 { + font-size: 15px; + position: relative; + color: #696E72; + text-align: center; + font-weight: 500; + text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.8); +} +@media (max-width: 991px) { + .pricing-3-options-section .contact h4 { + line-height: 26px; + } +} +.pricing-3-options-section .contact h4 span { + position: absolute; + font-size: 23px; + top: -5px; + left: 72px; + color: #626E7A; +} +@media (max-width: 991px) { + .pricing-3-options-section .contact h4 span { + position: relative; + left: 0px; + top: 5px; + } +} + +.pricing-dark-section { + border-top: 1px solid #EAEAEA; + border-bottom: 1px solid #EAEAEA; + background: #5E7A9B; + background: -webkit-linear-gradient(top, #355069 0, #5E7A9B 100%); + background: -moz-linear-gradient(top, #355069 0, #5E7A9B 100%); + background: -o-linear-gradient(top, #355069 0, #5E7A9B 100%); + background: linear-gradient(top, #355069 0, #5E7A9B 100%); + margin-top: 120px; + padding-top: 45px; + padding-bottom: 70px; +} +.pricing-dark-section .header { + text-align: center; +} +@media (max-width: 991px) { + .pricing-dark-section .header { + margin-bottom: 25px; + } +} +.pricing-dark-section .header h3 { + font-size: 27px; + color: #fff; + line-height: 25px; + font-weight: 400; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} +@media (max-width: 767px) { + .pricing-dark-section .header h3 { + font-size: 24px; + line-height: 29px; + } +} +.pricing-dark-section .header p { + font-size: 16px; + margin-top: 15px; + color: #F0F0F0; + font-weight: 400; + line-height: 25px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} +.pricing-dark-section .charts { + margin-top: 35px; +} +@media (max-width: 991px) { + .pricing-dark-section .charts { + margin-top: 10px; + } +} +.pricing-dark-section .charts .chart { + background-color: #fff; + border: 1px solid #E6E6E6; + border-radius: 5px; + box-shadow: 0px 0px 2px 0px rgba(181, 181, 181, 0.3); + padding: 20px 40px; + position: relative; + text-align: center; + width: 97%; + min-height: 315px; + position: relative; + top: 37px; +} +@media (max-width: 991px) { + .pricing-dark-section .charts .chart { + float: none !important; + left: 0px !important; + top: 0px !important; + margin: 0 auto; + width: 55%; + margin-bottom: 25px !important; + } + .pricing-dark-section .charts .chart.last { + margin-bottom: 0px !important; + } +} +@media (max-width: 600px) { + .pricing-dark-section .charts .chart { + width: 90% !important; + } +} +.pricing-dark-section .charts .chart.first { + float: right; + left: 12px; +} +.pricing-dark-section .charts .chart.last { + left: -12px; +} +.pricing-dark-section .charts .chart.featured { + min-height: 350px; + top: 0px; + margin: 0 auto; +} +.pricing-dark-section .charts .chart.featured .popular { + text-align: center; + text-transform: uppercase; + font-size: 16px; + letter-spacing: 1px; + color: #B1B1B1; + font-weight: 400; +} +.pricing-dark-section .charts .chart.featured .quantity { + margin-top: 15px; +} +.pricing-dark-section .charts .chart .quantity { + text-align: center; +} +.pricing-dark-section .charts .chart .quantity .dollar { + font-size: 19px; + position: relative; + top: -18px; +} +.pricing-dark-section .charts .chart .quantity .price { + font-size: 49px; +} +.pricing-dark-section .charts .chart .quantity .period { + font-size: 17px; + position: relative; + top: -8px; + margin-left: 4px; +} +.pricing-dark-section .charts .chart .plan-name { + text-align: center; + font-size: 20px; + font-weight: 400; + color: #777; + border-bottom: 1px solid #d5d5d5; + padding-bottom: 15px; + width: 90%; + margin: 0 auto; + margin-top: 8px; +} +.pricing-dark-section .charts .chart .specs { + margin-top: 20px; +} +.pricing-dark-section .charts .chart .specs .spec { + font-size: 15px; + color: #353535; + text-align: center; + font-weight: 300; + margin-bottom: 13px; +} +.pricing-dark-section .charts .chart .specs .spec .variable { + color: #1FBEE7; + font-weight: 400; +} +.pricing-dark-section .charts .chart .btn-signup { + margin-top: 20px; + height: 38px; +} + +.pricing-4-options-section { + margin-top: 55px; + margin-bottom: 150px; +} +.pricing-4-options-section .header { + text-align: center; +} +.pricing-4-options-section .header h3 { + font-size: 24px; + color: #444; + line-height: 25px; + font-weight: 500; +} +.pricing-4-options-section .header p { + font-size: 16px; + font-weight: 400; + color: #888; +} +@media (min-width: 992px) { + .pricing-4-options-section .container { + width: 1060px; + } +} +.pricing-4-options-section .charts { + margin-top: 50px; +} +.pricing-4-options-section .charts .basic-plan { + background-color: #fff; + border: 1px solid #E6E6E6; + border-radius: 5px; + box-shadow: 0px 0px 2px 0px rgba(181, 181, 181, 0.3); + padding: 20px 30px 10px 30px; + text-align: center; + width: 220px; + position: absolute; + right: 0px; + top: 20px; +} +@media (max-width: 991px) { + .pricing-4-options-section .charts .basic-plan { + float: none !important; + left: 0px !important; + top: 0px !important; + margin: 0 auto; + width: 55%; + margin-bottom: 25px !important; + position: relative; + margin-top: 30px; + } + .pricing-4-options-section .charts .basic-plan.last { + margin-bottom: 0px !important; + } +} +@media (max-width: 600px) { + .pricing-4-options-section .charts .basic-plan { + width: 90% !important; + } +} +.pricing-4-options-section .charts .basic-plan .plan-name { + text-align: center; + font-size: 17px; + font-weight: 400; + color: #444; + width: 90%; + margin: 0 auto; + margin-top: 5px; +} +.pricing-4-options-section .charts .basic-plan .quantity { + text-align: center; + border-bottom: 1px solid #d5d5d5; + margin-top: 5px; +} +.pricing-4-options-section .charts .basic-plan .quantity .dollar { + font-size: 19px; + position: relative; + top: -18px; +} +.pricing-4-options-section .charts .basic-plan .quantity .price { + font-size: 42px; +} +.pricing-4-options-section .charts .basic-plan .quantity .period { + font-size: 17px; + position: relative; + top: -8px; + margin-left: 4px; +} +.pricing-4-options-section .charts .basic-plan .description { + margin-top: 15px; + font-size: 13px; + color: #444; + line-height: 20px; +} +.pricing-4-options-section .charts .basic-plan .description a { + display: block; + margin-top: 20px; + font-weight: 500; + font-size: 15px; +} +.pricing-4-options-section .charts .chart { + background-color: #fff; + border: 1px solid #E6E6E6; + border-radius: 5px; + box-shadow: 0px 0px 2px 0px rgba(181, 181, 181, 0.3); + padding: 20px 40px; + position: relative; + text-align: center; + width: 280px; + float: left; + min-height: 315px; + position: relative; + top: 20px; +} +@media (min-width: 992px) { + .pricing-4-options-section .charts .chart.featured { + left: -25px; + } + .pricing-4-options-section .charts .chart.last { + left: -45px; + } +} +@media (max-width: 991px) { + .pricing-4-options-section .charts .chart { + float: none !important; + left: 0px !important; + top: 0px !important; + margin: 0 auto; + width: 55%; + margin-bottom: 25px !important; + } + .pricing-4-options-section .charts .chart.last { + margin-bottom: 0px !important; + } +} +@media (max-width: 600px) { + .pricing-4-options-section .charts .chart { + width: 90% !important; + } +} +.pricing-4-options-section .charts .chart.featured { + z-index: 999; +} +@media (min-width: 992px) { + .pricing-4-options-section .charts .chart.featured { + margin: 0 auto; + top: 0px; + min-height: 445px; + } + .pricing-4-options-section .charts .chart.featured .btn-signup { + margin-top: 32px; + } +} +.pricing-4-options-section .charts .chart.featured .popular { + position: absolute; + top: 0px; + left: 0px; +} +.pricing-4-options-section .charts .chart.featured .quantity { + margin-top: 15px; + padding-bottom: 5px; +} +.pricing-4-options-section .charts .chart .plan-name { + text-align: center; + font-size: 20px; + font-weight: 400; + color: #444; + width: 90%; + margin: 0 auto; + margin-top: 5px; +} +.pricing-4-options-section .charts .chart .quantity { + text-align: center; + border-bottom: 1px solid #d5d5d5; + margin-top: 10px; +} +.pricing-4-options-section .charts .chart .quantity .dollar { + font-size: 19px; + position: relative; + top: -18px; +} +.pricing-4-options-section .charts .chart .quantity .price { + font-size: 49px; +} +.pricing-4-options-section .charts .chart .quantity .period { + font-size: 17px; + position: relative; + top: -8px; + margin-left: 4px; +} +.pricing-4-options-section .charts .chart .specs { + margin-top: 20px; +} +.pricing-4-options-section .charts .chart .specs .spec { + font-size: 15px; + color: #353535; + text-align: center; + font-weight: 300; + margin-bottom: 13px; +} +.pricing-4-options-section .charts .chart .specs .spec .variable { + color: #1FBEE7; + font-weight: 400; +} +.pricing-4-options-section .charts .chart .btn-signup { + margin-top: 20px; + height: 38px; +} + +.pricing-tabs-section { + margin-top: 60px; +} +.pricing-tabs-section .header { + text-align: center; +} +.pricing-tabs-section .header h3 { + font-size: 23px; + color: #444; + line-height: 25px; + font-weight: 500; +} +.pricing-tabs-section .header p { + font-size: 16px; + font-weight: 400; + color: #888; +} +.pricing-tabs-section .tabs { + background: #F8F8F8; + text-align: center; + border-radius: 3px; + margin-top: 40px; +} +.pricing-tabs-section .tabs .tab { + background-color: #F8F8F8; + font-weight: 500; + font-size: 15px; + color: #555; + display: inline-block; + padding: 18px 25px; + margin-top: 10px; + cursor: pointer; + -webkit-transition: background-color 0.2s linear; + -moz-transition: background-color 0.2s linear; + -ms-transition: background-color 0.2s linear; + -o-transition: background-color 0.2s linear; + transition: background-color 0.2s linear; +} +.pricing-tabs-section .tabs .tab.anually { + padding: 18px 18px 18px 23px; +} +.pricing-tabs-section .tabs .tab.active { + background-color: #fff; + border: 1px solid #E8E8E8; + border-radius: 5px 5px 0px 0px; + border-bottom: 0px; +} +.pricing-tabs-section .tabs .tab span { + position: relative; + top: -2px; +} +.pricing-tabs-section .tabs .tab small { + color: #32ACDA; + margin-left: 3px; +} +.pricing-tabs-section .plans { + margin-top: 30px; + border: 1px solid #E3E3E3; + border-radius: 5px; +} +.pricing-tabs-section .plans .plan { + border-right: 0px; + min-height: 220px; +} +.pricing-tabs-section .plans .plan.middle { + border-right: 1px solid #E3E3E3; + border-left: 1px solid #E3E3E3; +} +.pricing-tabs-section .plans .plan .header { + text-align: left; + padding: 18px 25px; + border-bottom: 1px solid #E3E3E3; + margin: 0px -15px; + font-weight: 500; + font-size: 15px; +} +.pricing-tabs-section .plans .plan .header .icon { + color: #B7C5D8; + font-size: 18px; + position: relative; + top: 2px; + left: -4px; +} +.pricing-tabs-section .plans .plan .header .price { + float: right; + color: #3CA2E5; +} +.pricing-tabs-section .plans .plan .specs { + margin-top: 25px; + padding: 0px 10px 17px 10px; +} +.pricing-tabs-section .plans .plan .specs .spec { + font-size: 15px; + color: #666; + margin-bottom: 13px; +} +.pricing-tabs-section .plans .plan .specs .spec .variable { + color: #1FBEE7; + font-weight: 400; +} +.pricing-tabs-section .signup { + margin-top: 50px; + text-align: center; +} +.pricing-tabs-section .signup .button { + padding: 16px 38px; + font-size: 15px; + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #4ABCF3), color-stop(100%, #1a80d9)); + background: -webkit-linear-gradient(#4ABCF3, #1a80d9); + background: -moz-linear-gradient(#4ABCF3, #1a80d9); + background: -o-linear-gradient(#4ABCF3, #1a80d9); + background: linear-gradient(#4ABCF3, #1a80d9); +} +.pricing-tabs-section .signup .message { + text-align: center; + margin-top: 20px; + font-size: 12px; + color: #888; +} + +.pricing-comparison-section .divider { + height: 1px; + width: 100%; + background-color: #ddd; + position: relative; + margin: 120px 0 60px; +} +.pricing-comparison-section .divider:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 2px; + background-image: -webkit-gradient(linear, left top, right top, color-stop(0, rgba(255, 255, 255, 0.75)), color-stop(0.5, rgba(250, 250, 250, 0)), color-stop(1, rgba(255, 255, 255, 0.75))); + background-image: -webkit-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: -moz-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: -o-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: -ms-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); +} +.pricing-comparison-section .header { + text-align: center; +} +.pricing-comparison-section .header h3 { + font-size: 24px; + color: #444; + line-height: 25px; + font-weight: 500; +} +.pricing-comparison-section .header p { + font-size: 16px; + font-weight: 400; + color: #888; +} +.pricing-comparison-section .chart { + margin-top: 40px; + border: 1px solid #dfdfdf; + border-radius: 5px; + box-shadow: 0px 2px 3px 1px rgba(0, 0, 0, 0.1); +} +.pricing-comparison-section .chart .header { + text-align: center; + background: #fbfbfb; + margin: 0px -15px; + padding: 20px 35px; + font-weight: 500; + font-size: 17px; + color: #444; + border-bottom: 1px solid #dfdfdf; + height: 73px; +} +@media (max-width: 991px) { + .pricing-comparison-section .chart .header { + font-size: 15px; + } +} +@media (max-width: 767px) { + .pricing-comparison-section .chart .header { + font-size: 13px; + } +} +.pricing-comparison-section .chart .header .price { + color: #1a80d9; + position: relative; + top: 3px; + font-size: 14px; +} +@media (max-width: 767px) { + .pricing-comparison-section .chart .header .price { + font-size: 13px; + } +} +.pricing-comparison-section .chart .features-col .header { + text-align: left; + border-radius: 5px 0px 0px 0px; + margin-bottom: 30px; + padding: 23px 30px; + font-size: 18px; +} +@media (max-width: 991px) { + .pricing-comparison-section .chart .features-col .header { + font-size: 16px; + } +} +.pricing-comparison-section .chart .features-col .spec { + margin-bottom: 32px; + padding-left: 40px; + position: relative; +} +.pricing-comparison-section .chart .features-col .spec .icon { + position: absolute; + font-size: 27px; + color: #849FC4; + left: 0px; + top: -4px; +} +.pricing-comparison-section .chart .plan-col { + border-left: 1px solid #dfdfdf; +} +.pricing-comparison-section .chart .plan-col.last .header { + border-radius: 0px 5px 0px 0px; +} +.pricing-comparison-section .chart .plan-col .header { + padding: 13px 0px; +} +.pricing-comparison-section .chart .plan-col .spec { + margin-bottom: 34px; + margin-top: 25px; +} +.pricing-comparison-section .chart .plan-col .spec .check { + margin: 0 auto; + display: block; + max-width: 20px; +} +.pricing-comparison-section .chart .plan-col .button { + margin-top: 20px; + margin-bottom: 15px; + font-size: 13px; + padding: 11px 24px; + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #4ABCF3), color-stop(100%, #1a80d9)); + background: -webkit-linear-gradient(#4ABCF3, #1a80d9); + background: -moz-linear-gradient(#4ABCF3, #1a80d9); + background: -o-linear-gradient(#4ABCF3, #1a80d9); + background: linear-gradient(#4ABCF3, #1a80d9); +} +@media (max-width: 991px) { + .pricing-comparison-section .chart .plan-col .button { + padding: 6px 10px; + text-align: center; + } +} +.pricing-comparison-section .message p { + text-align: center; + font-size: 13px; + color: #656565; + margin-top: 30px; +} +.pricing-comparison-section .faq { + margin-top: 50px; +} +.pricing-comparison-section .faq .header h3 { + font-size: 20px; + color: #454545; + text-align: left; +} +.pricing-comparison-section .faq .questions { + margin-top: 25px; +} +.pricing-comparison-section .faq .questions .question { + margin-bottom: 40px; +} +@media (min-width: 992px) { + .pricing-comparison-section .faq .questions .question { + width: 90%; + } +} +.pricing-comparison-section .faq .questions .question strong { + font-weight: 500; + font-size: 16px; + color: #454545; +} +.pricing-comparison-section .faq .questions .question p { + color: #555; + margin-top: 10px; + font-size: 13px; + line-height: 22px; +} + +.about-us-slider { + margin-top: 60px; +} +.about-us-slider .header { + text-align: left; +} +.about-us-slider .header h3 { + font-size: 26px; + color: #444; + line-height: 25px; + font-weight: 500; +} +.about-us-slider .header p { + font-size: 15px; + font-weight: 400; + color: #666; + margin-top: 20px; + line-height: 24px; +} +@media (min-width: 992px) { + .about-us-slider .header p { + width: 48%; + } +} +.about-us-slider .flexslider { + margin-top: 40px; + box-shadow: none; + border-radius: 0px; +} +.about-us-slider .flexslider .slides img { + max-height: 400px; +} +.about-us-slider .flexslider .flex-control-paging { + bottom: -35px; +} +.about-us-slider .flexslider .flex-control-paging li { + margin: 0 7px; +} +.about-us-slider .flexslider .flex-control-paging li a { + background: #e6e6e6; + box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.3); + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.about-us-slider .flexslider .flex-control-paging li a.flex-active { + background: #3785b9; +} + +.about-us-team { + margin-top: 40px; +} +.about-us-team h1 { + font-size: 24px; + color: #444; + line-height: 25px; + font-weight: 500; + margin-bottom: 25px; +} +.about-us-team p { + color: #666; + line-height: 23px; + margin-bottom: 15px; +} +@media (min-width: 992px) { + .about-us-team p { + width: 88%; + } +} +.about-us-team .join-team { + padding: 8px 26px; + font-size: 13px; + margin-top: 10px; +} +.about-us-team .stats { + margin-top: 60px; + font-size: 16px; + color: #757575; +} +.about-us-team .stats strong { + font-size: 33px; + color: #656565; + font-weight: 500; + position: relative; + top: 1px; + margin-right: 5px; +} +.about-us-team .stats .col-sm-3 { + border-bottom: 1px solid #efefef; + padding-bottom: 25px; +} +@media (max-width: 767px) { + .about-us-team .stats .col-sm-3 { + padding-bottom: 10px; + margin-bottom: 20px; + } +} +.about-us-team .team { + margin-top: 90px; +} +.about-us-team .team .team-row { + text-align: center; + margin-bottom: 35px; +} +.about-us-team .team img { + margin: 0px 20px; + max-width: 90px; + border-radius: 10px; + position: relative; + top: 0px; + box-shadow: 0px 2px 1px 0px rgba(0, 0, 0, 0.4); + -webkit-transition: top 0.15s linear; + -moz-transition: top 0.15s linear; + -ms-transition: top 0.15s linear; + -o-transition: top 0.15s linear; + transition: top 0.15s linear; +} +@media (max-width: 991px) { + .about-us-team .team img { + margin: 0px 10px 20px 0; + } +} +.about-us-team .team img:hover { + top: -5px; + box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.5); +} +.about-us-team .team .tooltip-inner { + padding: 12px 18px; + font-size: 13px; + background: rgba(0, 0, 0, 0.8); + border: 1px solid #000; + border-bottom: 0px; +} +.about-us-team .team .tooltip-arrow { + border-top-color: rgba(0, 0, 0, 0.8); +} + +.contact-us-map { + position: relative; + top: -20px; + box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.35); +} +.contact-us-map #map { + height: 450px; + width: 100%; +} +.contact-us-map #directions { + position: absolute; + top: 50px; + left: 13%; + text-align: center; + width: 300px; + background: rgba(255, 255, 255, 0.9); + border-radius: 5px; + box-shadow: 0px 4px 15px 0px rgba(0, 0, 0, 0.2); + padding: 30px 20px 30px 20px; +} +@media (max-width: 767px) { + .contact-us-map #directions { + display: none; + } +} +.contact-us-map #directions p { + font-size: 19px; + color: #333; + margin-bottom: 15px; +} +.contact-us-map #directions button { + margin-top: 15px; +} + +.contact-us-info { + margin-top: 50px; +} +.contact-us-info .message h3 { + color: #444; + font-size: 24px; + font-weight: 500; +} +.contact-us-info .message p { + font-size: 15px; + font-weight: 400; + color: #777; +} +.contact-us-info .message form { + margin-top: 35px; + width: 80%; + /* error messages from jquery validate */ +} +@media (max-width: 767px) { + .contact-us-info .message form { + width: 100%; + } +} +.contact-us-info .message form input[type=text], +.contact-us-info .message form input[type=email] { + width: 70%; +} +@media (max-width: 767px) { + .contact-us-info .message form input[type=text], + .contact-us-info .message form input[type=email] { + width: 100%; + } +} +.contact-us-info .message form label { + color: #555; + font-weight: 500; + margin-bottom: 7px; +} +.contact-us-info .message form .button { + margin-top: 20px; +} +.contact-us-info .message form label.error { + position: relative !important; + display: inline-block !important; + margin-top: -2px !important; + margin-bottom: -8px !important; + color: #fff !important; + font-weight: normal !important; + font-size: 15px !important; + padding: 5px 7px !important; + background: #4B9FCF !important; + border-radius: 4px !important; +} +.contact-us-info .message form label.error.valid { + display: none !important; +} +.contact-us-info .message form label.error:after { + bottom: 100%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-bottom-color: #4B9FCF; + border-width: 5px; + left: 30px; + margin-left: -5px; +} +@media (max-width: 991px) { + .contact-us-info .contact { + margin-top: 50px; + } +} +.contact-us-info .contact .address { + margin-bottom: 30px; +} +.contact-us-info .contact h3 { + color: #444; + font-size: 18px; + font-weight: 500; +} +.contact-us-info .contact p { + font-size: 15px; + font-weight: 400; + color: #777; +} +.contact-us-info .contact .online-support { + background: #f8fdfe; + border: 1px solid #bee1f3; + margin-top: 40px; + border-radius: 6px; + padding: 20px 22px 18px 22px; + width: 70%; +} +.contact-us-info .contact .online-support strong { + font-weight: 500; + color: #333; + font-size: 15px; +} +.contact-us-info .contact .online-support p { + color: #1f95d0; + margin-top: 14px; + line-height: 18px; +} +.contact-us-info .contact .social { + margin-top: 25px; +} +.contact-us-info .contact .social a { + border: 0px; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.contact-us-info .contact .social a.tw { + position: relative; + top: 2px; +} +.contact-us-info .contact .social a:hover { + opacity: 0.8; + filter: alpha(opacity=80); +} + +.status-page { + margin-top: 40px; + margin-bottom: 130px; +} +.status-page .header { + text-align: center; +} +.status-page .header h3 { + font-size: 22px; + color: #444; + line-height: 25px; + font-weight: 500; + margin-bottom: 7px; +} +.status-page .header p { + font-size: 16px; + font-weight: 400; + color: #888; +} +.status-page .current-status { + background: #F7F8FB; + border: 1px solid #EAEDF7; + border-radius: 8px; + box-shadow: inset rgba(100, 100, 100, 0.25) 0 1px 1px; + padding: 20px 30px; + margin: 0 auto; + margin-top: 35px; +} +.status-page .current-status .updated { + float: right; + color: #616A72; + font-size: 13px; +} +@media (max-width: 767px) { + .status-page .current-status .updated { + float: none; + text-align: center; + display: block; + margin-bottom: 20px; + } +} +.status-page .current-status .status { + font-size: 18px; + font-weight: 500; +} +.status-page .current-status .status .color { + width: 15px; + height: 15px; + border-radius: 50px; + float: left; + position: relative; + top: 5px; + margin-right: 20px; +} +.status-page .current-status .status .color.green { + background: #96cb4c; + border: 1px solid #8BC43D; + box-shadow: 0px 0px 7px 2px #B3E074, inset 0px 2px 1px -1px rgba(255, 255, 255, 0.8); +} +.status-page .current-status .status .color.yellow { + background: #F0D775; + border: 1px solid #E0CC7E; + box-shadow: 0px 0px 7px 2px #F0D775, inset 0px 2px 1px -1px rgba(255, 255, 255, 0.8); +} +.status-page .current-status .status .color.red { + background: #F56B65; + border: 1px solid #FC7D77; + box-shadow: 0px 0px 7px 2px #FF9B96, inset 0px 2px 1px -1px rgba(255, 255, 255, 0.8); +} +.status-page .current-status .help { + margin-top: 7px; + margin-left: 35px; +} +.status-page .modules { + margin-top: 40px; + border-radius: 5px; +} +.status-page .modules > .list-group .list-group-item { + padding: 13px 15px; +} +.status-page .modules > .list-group .list-group-item:first-child { + border-top: 0px; + border-top-right-radius: 5px; + border-top-left-radius: 5px; +} +.status-page .modules > .list-group .list-group-item .status { + float: right; + color: #0F9B0F; + font-size: 13px; + font-weight: 500; +} +.status-page .modules > .list-group .list-group-item .status.down { + color: #D88C1B; +} +.status-page .messages { + margin-top: 35px; +} +.status-page .messages h3 { + font-size: 18px; + color: #444; + margin-bottom: 25px; +} +.status-page .messages .date { + margin-bottom: 25px; +} +.status-page .messages .date .day { + font-size: 13px; + font-weight: bold; + color: #454545; + padding-bottom: 6px; + margin-bottom: 6px; + border-bottom: 1px solid #eee; +} +.status-page .messages .date .ok { + color: #0F9B0F; + font-size: 13px; + font-weight: 500; +} +.status-page .messages .date .issues { + color: #D88C1B; + font-size: 13px; + font-weight: 500; + margin-bottom: 18px; +} +.status-page .messages .date .update { + color: #333; + font-size: 13px; +} +.status-page .messages .date .update strong { + font-size: 13px; + font-weight: bold; + color: #454545; + min-width: 93px; + display: inline-block; +} +.status-page .full-history { + margin-top: 50px; +} + +.gallery-showcase { + margin-top: 60px; +} +.gallery-showcase .header { + text-align: left; +} +.gallery-showcase .header h3 { + font-size: 26px; + color: #596166; + line-height: 25px; + font-weight: 500; +} +.gallery-showcase .header p { + font-size: 15px; + font-weight: 400; + color: #666; + margin-top: 12px; + line-height: 24px; +} +@media (min-width: 992px) { + .gallery-showcase .header p { + width: 48%; + } +} +.gallery-showcase .filtering { + margin-top: 35px; + margin-bottom: 35px; +} +.gallery-showcase .filtering #filters { + padding-left: 20px; + text-align: center; + border-bottom: 2px solid #EDEDED; +} +@media (max-width: 767px) { + .gallery-showcase .filtering #filters { + display: none; + } +} +.gallery-showcase .filtering #filters li { + list-style-type: none; + display: inline-block; + margin: 0px 8px; +} +.gallery-showcase .filtering #filters li:first-child { + margin-left: 0px; +} +.gallery-showcase .filtering #filters li a { + color: #888; + font-weight: 400; + border: 0px; + padding: 10px 22px; + font-size: 16px; + position: relative; + -webkit-transition: color 0.25s linear; + -moz-transition: color 0.25s linear; + -ms-transition: color 0.25s linear; + -o-transition: color 0.25s linear; + transition: color 0.25s linear; +} +.gallery-showcase .filtering #filters li a.active { + color: #2B69A2; + font-weight: 400; + border-bottom: 0px; + display: inline-block; +} +.gallery-showcase .filtering #filters li a.active:after { + content: ""; + position: absolute; + width: 100%; + height: auto; + border-bottom: 2px solid #7BA3C9; + left: 1px; + bottom: -2px; +} +.gallery-showcase .filtering #filters li a:hover { + color: #2B69A2; + background: none; + text-decoration: none; +} +.gallery-showcase .gallery_container { + overflow: visible !important; +} +.gallery-showcase .pic { + margin-bottom: 40px; + position: relative; + display: block; + -webkit-transition: 0.5s cubic-bezier(0.25, 1.45, 0.51, 1.13); + -moz-transition: 0.5s cubic-bezier(0.25, 1.45, 0.51, 1.13); + -ms-transition: 0.5s cubic-bezier(0.25, 1.45, 0.51, 1.13); + -o-transition: 0.5s cubic-bezier(0.25, 1.45, 0.51, 1.13); + transition: 0.5s cubic-bezier(0.25, 1.45, 0.51, 1.13); +} +.gallery-showcase .pic.fluidbox-opened:hover .layer { + background: rgba(0, 0, 0, 0); +} +.gallery-showcase .pic.fluidbox-opened:hover .layer p { + opacity: 0; + filter: alpha(opacity=0); +} +.gallery-showcase .pic:hover { + text-decoration: none; +} +.gallery-showcase .pic:hover .layer { + background: rgba(33, 33, 33, 0.8); +} +.gallery-showcase .pic:hover .layer p { + margin-top: -20px; + opacity: 1; + filter: alpha(opacity=100); +} +.gallery-showcase .pic .layer { + width: 100%; + height: 100%; + position: absolute; + z-index: 9; + top: 0px; + left: 0px; + background: rgba(0, 0, 0, 0); + -webkit-transition: all 0.25s ease-out; + -moz-transition: all 0.25s ease-out; + -ms-transition: all 0.25s ease-out; + -o-transition: all 0.25s ease-out; + transition: all 0.25s ease-out; +} +.gallery-showcase .pic .layer p { + transition: margin-top 0.35s ease-out, opacity 0.55s ease-out; + color: #fff; + font-size: 15px; + text-align: center; + top: 50%; + width: 100%; + position: absolute; + font-weight: 600; + margin-top: 25px; + opacity: 0; + filter: alpha(opacity=0); +} +.gallery-showcase .pic .layer p span { + position: relative; + top: 1px; + margin-right: 2px; +} +.gallery-showcase .pic img { + margin: 0 auto; +} + +/* Fluidbox styling starts here */ +a[data-fluidbox] { + background-color: #eee; + border: none; + cursor: -webkit-zoom-in; + cursor: -moz-zoom-in; + margin-bottom: 1.5rem; +} +a[data-fluidbox].fluidbox-opened { + cursor: -webkit-zoom-out; + cursor: -moz-zoom-out; +} +a[data-fluidbox] img { + display: block; + margin: 0 auto; + opacity: 0; + filter: alpha(opacity=0); + max-width: 100%; + transition: all 0.25s ease-in-out; +} + +a[class^=float] { + margin: 1rem; + margin-top: 0; + width: 33.33333%; +} +a[class^=float].float-left { + float: left; + margin-left: 0; +} +a[class^=float].float-right { + float: right; + margin-right: 0; +} + +#fluidbox-overlay { + background-color: rgba(255, 255, 255, 0.85); + cursor: pointer; + cursor: -webkit-zoom-out; + cursor: -moz-zoom-out; + display: none; + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + z-index: 500; +} + +.fluidbox-wrap { + background-position: center center; + background-size: cover; + margin: 0 auto; + position: relative; + z-index: 400; + transition: all 0.25s ease-in-out; +} +.fluidbox-opened .fluidbox-wrap { + z-index: 600; +} + +.fluidbox-ghost { + background-size: cover; + background-position: center center; + position: absolute; + transition: all 0.25s ease-in-out; +} + +html.portfolio-page { + background: #F5F9FF; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, rgba(255, 255, 255, 0)), color-stop(1, rgba(255, 255, 255, 0.5))); + background-image: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.5) 100%); + background-image: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.5) 100%); + background-image: -o-linear-gradient(top, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.5) 100%); + background-image: -ms-linear-gradient(top, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.5) 100%); + background-image: linear-gradient(top, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.5) 100%); + -webkit-background-size: 100% 30%; + -moz-background-size: 100% 30%; + background-size: 100% 30%; + background-repeat: no-repeat; + background-position: 0 100%; +} + +.portfolio-body { + background: none; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(25%, rgba(255, 255, 255, 0))); + background-image: -webkit-linear-gradient(top, #fff 0, rgba(255, 255, 255, 0) 25%); + background-image: -moz-linear-gradient(top, #fff 0, rgba(255, 255, 255, 0) 25%); + background-image: -o-linear-gradient(top, #fff 0, rgba(255, 255, 255, 0) 25%); + background-image: -ms-linear-gradient(top, #fff 0, rgba(255, 255, 255, 0) 25%); + background-image: linear-gradient(top, #fff 0, rgba(255, 255, 255, 0) 25%); +} + +.portfolio-showcase { + margin-top: 50px; +} +.portfolio-showcase .header { + text-align: left; + margin-bottom: 40px; +} +.portfolio-showcase .header h3 { + font-size: 28px; + color: #596166; + line-height: 25px; + font-weight: 500; +} +.portfolio-showcase .header p { + font-size: 16px; + font-weight: 400; + color: #8D959B; + line-height: 24px; +} +@media (min-width: 992px) { + .portfolio-showcase .header p { + width: 48%; + } +} +.portfolio-showcase .project { + background: #fff; + padding: 40px 60px; + border-radius: 5px; + margin-bottom: 35px; + box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.05), 0 1px 0 rgba(0, 0, 0, 0.05), -1px 0 0 rgba(0, 0, 0, 0.04), 1px 0 0 rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.12); +} +@media (max-width: 767px) { + .portfolio-showcase .project { + padding: 40px 35px 35px 35px; + } +} +.portfolio-showcase .project h3 { + color: #585F64; + font-size: 22px; + margin-top: 0px; + text-align: center; +} +.portfolio-showcase .project .screen { + margin-top: 45px; + text-align: center; +} +.portfolio-showcase .project .screen img { + margin: 0 auto; +} +.portfolio-showcase .project .description { + margin-top: 40px; + color: #7D8186; + line-height: 21px; +} +.portfolio-showcase .project .divider { + height: 1px; + width: 100%; + background-color: #ddd; + position: relative; + margin-top: 45px; +} +.portfolio-showcase .project .divider:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 2px; + background-image: -webkit-gradient(linear, left top, right top, color-stop(0, rgba(255, 255, 255, 0.75)), color-stop(0.5, rgba(250, 250, 250, 0)), color-stop(1, rgba(255, 255, 255, 0.75))); + background-image: -webkit-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: -moz-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: -o-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: -ms-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); + background-image: linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250, 250, 250, 0) 50%, rgba(255, 255, 255, 0.75) 100%); +} +.portfolio-showcase .project .visit { + margin-top: 30px; + text-align: center; +} + +.portfolio-item-showcase { + margin-top: 40px; + margin-bottom: -30px; +} +.portfolio-item-showcase .header { + text-align: left; + margin-bottom: 25px; +} +.portfolio-item-showcase .header h3 { + display: inline-block; + font-size: 17px; + font-family: Myriad Pro, Lato, Helvetica Neue, Arial; + color: #53A6DA; + margin-top: 0px; + line-height: 25px; + font-weight: 500; + margin-bottom: 5px; + cursor: pointer; +} +.portfolio-item-showcase .project h1 { + text-align: center; + font-size: 25px; + color: #647583; + margin-top: 10px; + margin-bottom: 70px; +} +.portfolio-item-showcase .project .screens img { + margin: 0 auto; + margin-bottom: 80px; + margin-top: 10px; +} +.portfolio-item-showcase .project .screens img.magnifier { + width: 490px; + margin-bottom: 100px; +} +.portfolio-item-showcase .project .screens img.mobile { + margin-bottom: 110px; +} +.portfolio-item-showcase .project .screens .text { + color: #7D8186; + font-size: 15px; + line-height: 22px; + margin-bottom: 80px; +} +.portfolio-item-showcase .project .description .info { + margin-top: 25px; +} +@media (min-width: 992px) { + .portfolio-item-showcase .project .description .info.stick { + position: fixed; + top: 10px; + } +} +.portfolio-item-showcase .project .description .info .icons { + margin-bottom: 20px; +} +.portfolio-item-showcase .project .description .info .icons .ion-icon:before { + color: #b2c3cb; + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b2c3cb), color-stop(100%, #DBE3E7)); + background: -webkit-linear-gradient(#b2c3cb, #DBE3E7); + font-size: 60px; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + margin: 0 9px; +} +.portfolio-item-showcase .project .description .info .icons .ion-social-windows:before { + font-size: 50px; +} +.portfolio-item-showcase .project .description .info p { + color: #7D8186; + line-height: 22px; + margin-bottom: 22px; +} +.portfolio-item-showcase .project .description .info .tweet-it { + color: #00A7DD; + margin-top: 3px; + display: inline-block; + position: relative; + padding-left: 29px; + text-decoration: none; +} +.portfolio-item-showcase .project .description .info .tweet-it:hover .ion-social-twitter:before { + content: "\f242"; +} +.portfolio-item-showcase .project .description .info .tweet-it .ion-social-twitter { + margin-right: 8px; + font-size: 21px; + position: absolute; + top: 0px; + left: 0px; +} + +.support-header { + margin-top: -20px; + background: #F4F7FA; + border-bottom: 1px solid #DCE4EC; + padding: 50px 0px; +} +@media (max-width: 767px) { + .support-header { + padding: 30px 0px; + text-align: center; + } +} +.support-header h2, .support-header p { + text-shadow: 1px 1px 1px #fff; +} +@media (max-width: 767px) { + .support-header h2 { + font-size: 27px; + } +} +.support-header h2 .entypo-lifebuoy { + position: absolute; + left: -25px; + top: 22px; + font-size: 29px; + color: #7AA3C5; +} +@media (max-width: 767px) { + .support-header h2 .entypo-lifebuoy { + position: relative; + left: 0px; + } +} +.support-header p { + font-size: 17px; + color: #5A5A5A; +} +@media (max-width: 767px) { + .support-header p { + font-size: 15px; + } +} +.support-header form { + margin-top: 30px; +} +.support-header form .input-group:before { + display: inline-block; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + content: "\f002"; + position: absolute; + top: 14px; + left: 13px; + font-size: 20px; + z-index: 99; + color: rgb(208, 218, 228); +} +.support-header form .form-control { + height: 50px; + font-size: 17px; + line-height: 20px; + padding-left: 42px; +} +.support-header form .form-control::-webkit-input-placeholder { + color: #BCC8D6; + font-size: 17px; + font-weight: 300; +} +.support-header form .form-control:-moz-placeholder { + color: #BCC8D6; + font-size: 17px; + font-weight: 300; +} +.support-header form .form-control::-moz-placeholder { + color: #BCC8D6; + font-size: 17px; + font-weight: 300; +} +.support-header form .form-control:-ms-input-placeholder { + color: #BCC8D6; + font-size: 17px; + font-weight: 300; +} +.support-header form .input-group-btn .btn { + padding: 14px 18px; +} + +.support-topics { + margin-top: 40px; +} +.support-topics h4 { + font-weight: 400; + margin-bottom: 50px; + font-size: 25px; +} +.support-topics .topic { + margin-bottom: 45px; +} +@media (max-width: 767px) { + .support-topics .topic { + text-align: center; + } +} +.support-topics .topic .icon { + display: block; + font-size: 45px; + color: #70CBF5; +} +.support-topics .topic a { + color: #454545; + text-decoration: none; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.support-topics .topic a strong { + display: block; + margin-top: 15px; + font-size: 16px; +} +.support-topics .topic a:hover { + color: #5CB9E4; +} +.support-topics .topic p { + margin-top: 17px; + color: #838383; +} + +.not-found-page { + height: 100%; + background: #3A4F68; + background: -moz-radial-gradient(center, ellipse cover, #4D6077 0%, #3A4F68 100%); + background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, #4D6077), color-stop(100%, #3A4F68)); + background: -webkit-radial-gradient(center, ellipse cover, #4D6077 0%, #3A4F68 100%); + background: -o-radial-gradient(center, ellipse cover, #4D6077 0%, #3A4F68 100%); + background: -ms-radial-gradient(center, ellipse cover, #4D6077 0%, #3A4F68 100%); + background: radial-gradient(ellipse at center, #4D6077 0%, #3A4F68 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = "#4D6077", endColorstr = "#3A4F68", GradientType = 1); +} + +#not-found { + overflow: hidden; +} +#not-found .info { + text-align: center; + padding-top: 100px; +} +#not-found .info h1 { + color: #fff; + font-size: 13em; + margin-bottom: 20px; + font-weight: 200; + letter-spacing: -3px; + text-shadow: 1px 1px rgba(0, 0, 0, 0.35); +} +#not-found .info p { + font-size: 23px; + color: #FFF; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.35); + margin-bottom: 0; +} +#not-found .info .go-back { + font-size: 16px; + text-shadow: none; + color: #9BB2CE; + z-index: 999; + position: absolute; + left: 0; + right: 0; + padding: 25px 0; + max-width: 400px; + margin: auto; + margin-top: -15px; +} +#not-found .info .go-back a { + color: #fff; +} +#not-found #container canvas { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +#not-found-alt { + overflow: hidden; +} +#not-found-alt .info { + text-align: center; + margin-top: 30px; + position: absolute; + width: 100%; +} +#not-found-alt .info h1 { + color: #fff; + font-size: 11em; + margin-bottom: 20px; + font-weight: 200; + letter-spacing: -3px; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.15); +} +#not-found-alt .info p { + font-size: 25px; + color: #4D728A; + margin-bottom: 0; +} +#not-found-alt .info .go-back { + font-size: 16px; + text-shadow: none; + letter-spacing: 0px; + color: #506379; + z-index: 999; + position: absolute; + left: 0; + right: 0; + padding: 25px 0; + max-width: 400px; + margin: auto; + margin-top: -15px; +} +#not-found-alt .info .go-back a { + color: #429AD5; + text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.4); + font-size: 18px; +} +#not-found-alt canvas { + background: linear-gradient(hsl(200, 50%, 80%) 0%, hsl(200, 30%, 95%) 75%); + display: block; +} +#not-found-alt #container { + height: 100%; + left: 0; + position: fixed; + top: 0; + width: 100%; +} + +.coming-soon-page { + border-top: 5px solid #4DB2E7; + height: 100vh; + background: #fff; + background: -moz-radial-gradient(center, ellipse cover, #fff 0%, #F4F9FF 100%); + background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, #fff), color-stop(100%, #F4F9FF)); + background: -webkit-radial-gradient(center, ellipse cover, #fff 0%, #F4F9FF 100%); + background: -o-radial-gradient(center, ellipse cover, #fff 0%, #F4F9FF 100%); + background: -ms-radial-gradient(center, ellipse cover, #fff 0%, #F4F9FF 100%); + background: radial-gradient(ellipse at center, #fff 0%, #F4F9FF 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = "#fff", endColorstr = "#F4F9FF", GradientType = 1); +} +.coming-soon-page.dark { + border-top: 5px solid #DAEEF8; + background: #4D576D !important; +} +.coming-soon-page.dark .info a { + color: #fff; +} +.coming-soon-page.dark .info h3 { + color: #fff; + font-weight: 300; +} +.coming-soon-page.dark #countdown #clock .flip-clock-divider .flip-clock-label { + color: #fff; +} +.coming-soon-page.dark form .button { + display: inline-block; + vertical-align: middle; + zoom: 1; + color: #fff; + padding: 9px 27px; + border: 2px solid #fff; + border-radius: 4px; + font-size: 15px; + font-weight: 400; + background: rgba(0, 0, 0, 0.08); + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: none; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.coming-soon-page.dark form .button:hover { + text-decoration: none; + color: #ddd; + border-color: #ddd; +} +.coming-soon-page.dark .social .ion span { + color: #AEDCFF; +} +.coming-soon-page .skins-nav { + opacity: 1; + -webkit-transition: left 0.3s; + -moz-transition: left 0.3s; + -ms-transition: left 0.3s; + -o-transition: left 0.3s; + transition: left 0.3s; + position: fixed; + left: -40px; + top: 135px; + font-size: 13px; + z-index: 9999; +} +.coming-soon-page .skins-nav:hover { + left: 0px; +} +.coming-soon-page .skins-nav a { + display: block; + color: #FFF; + text-decoration: none; + padding-right: 30px; + height: 37px; + border-radius: 0 4px 4px 0; + margin-bottom: 3px; + -webkit-transition: all 0.5s; + -moz-transition: all 0.5s; + -ms-transition: all 0.5s; + -o-transition: all 0.5s; + transition: all 0.5s; + text-align: right; + position: relative; + box-shadow: inset 0 1px rgba(255, 255, 255, 0.21); +} +.coming-soon-page .skins-nav a.light { + background: #9BD4F5; +} +.coming-soon-page .skins-nav a.dark { + background: #444; +} +.coming-soon-page .skins-nav a.active .ion { + display: block; +} +.coming-soon-page .skins-nav a .text { + white-space: nowrap; + display: block; + width: 85px; + position: relative; + top: 9px; + -webkit-transition: width 0.2s; + -moz-transition: width 0.2s; + -ms-transition: width 0.2s; + -o-transition: width 0.2s; + transition: width 0.2s; +} +.coming-soon-page .skins-nav a .ion { + position: absolute; + top: 11px; + right: 8px; + display: none; +} +.coming-soon-page .info { + margin-top: 60px; + text-align: center; + color: #fff; +} +.coming-soon-page .info a { + color: #5F5F5F; + text-decoration: none; + font-family: Myriad Pro, Lato, Helvetica Neue, Arial; + font-size: 46px; +} +.coming-soon-page .info a:hover { + text-decoration: none; +} +.coming-soon-page .info h3 { + text-align: center; + color: #4DB2E7; + text-transform: uppercase; + letter-spacing: 2px; + font-weight: 400; + font-size: 18px; + margin-top: 25px; +} +.coming-soon-page #countdown { + margin-top: 35px; +} +.coming-soon-page #countdown #clock { + width: 100%; + margin: 0 auto; +} +@media (min-width: 992px) { + .coming-soon-page #countdown #clock { + width: 47%; + } +} +@media (max-width: 991px) { + .coming-soon-page #countdown #clock { + max-width: 142px; + } + .coming-soon-page #countdown #clock ul { + margin-bottom: 50px; + } + .coming-soon-page #countdown #clock .flip-clock-divider { + height: inherit; + display: block; + } + .coming-soon-page #countdown #clock .flip-clock-divider:before, .coming-soon-page #countdown #clock .flip-clock-divider:after { + display: table; + content: ""; + } + .coming-soon-page #countdown #clock .flip-clock-divider.minutes .flip-clock-label, .coming-soon-page #countdown #clock .flip-clock-divider.seconds .flip-clock-label { + right: -74px !important; + } + .coming-soon-page #countdown #clock .flip-clock-divider .flip-clock-label { + bottom: -8.5em !important; + } +} +.coming-soon-page #countdown #clock .flip-clock-divider.minutes .flip-clock-label, .coming-soon-page #countdown #clock .flip-clock-divider.seconds .flip-clock-label { + right: -102px; +} +.coming-soon-page #countdown #clock .flip-clock-divider .flip-clock-label { + color: #555; + bottom: -1.5em; + top: inherit; + right: -92px; + font-size: 15px; +} +.coming-soon-page #countdown #clock .flip-clock-divider .flip-clock-dot { + display: none; +} +.coming-soon-page form { + margin-top: 110px; + text-align: center; +} +@media (max-width: 767px) { + .coming-soon-page form { + margin-top: 70px; + } +} +.coming-soon-page form input[type=email] { + width: 250px; + height: 43px; +} +@media (max-width: 767px) { + .coming-soon-page form input[type=email] { + margin: 0 auto; + } +} +.coming-soon-page form .button { + background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #38BAFF), color-stop(100%, #37ADEB)); + background: -webkit-linear-gradient(#38BAFF, #37ADEB); + background: -moz-linear-gradient(#38BAFF, #37ADEB); + background: -o-linear-gradient(#38BAFF, #37ADEB); + background: linear-gradient(#38BAFF, #37ADEB); + padding: 10px 19px; + font-size: 15px; + font-weight: 500; + border: 1px solid #54AEF5; +} +.coming-soon-page .social { + margin-top: 100px; + text-align: center; + margin-bottom: 20px; +} +.coming-soon-page .social .ion { + font-size: 34px; + margin: 0 10px; + display: inline-block; +} +.coming-soon-page .social .ion span { + color: #9DBDD6; +} +.coming-soon-page .social .ion .ion-social-facebook { + font-size: 25px; +} +.coming-soon-page .social .tm { + margin-top: 15px; + color: #B1B9C4; +} + +#docs #guide { + background: #F4F8FA; + border-right: 1px solid #DADFE5; + left: 0; + top: 0; + position: fixed; + overflow: hidden; + overflow-y: auto; + width: 210px; + z-index: 25; + height: 100%; +} +#docs #guide .logo { + margin-top: 0; + font-size: 29px; +} +#docs #guide .logo a { + color: #000; + font-family: Myriad Pro, Lato, Helvetica Neue, Arial; + padding: 20px 0 15px 23px; + display: block; + border-bottom: 1px solid #D6DCE0; + box-shadow: 0px 1px #FFF; + -webkit-transition: color 0.15s linear; + -moz-transition: color 0.15s linear; + -ms-transition: color 0.15s linear; + -o-transition: color 0.15s linear; + transition: color 0.15s linear; +} +#docs #guide .logo a:hover { + color: #858585; + text-decoration: none; +} +#docs #guide .menu > li.active > .nav { + display: block; +} +#docs #guide .menu > li.active > a { + background: #DEEAF1; +} +#docs #guide .menu > li a { + color: #3391C9; + font-weight: 500; + font-size: 13px; + padding: 10px 15px 10px 25px; + -webkit-transition: all 0.15s linear; + -moz-transition: all 0.15s linear; + -ms-transition: all 0.15s linear; + -o-transition: all 0.15s linear; + transition: all 0.15s linear; +} +#docs #guide .menu > li a:hover { + background: #DEEAF1; +} +#docs #guide .menu > li > .nav { + margin-bottom: 7px; + display: none; +} +#docs #guide .menu > li > .nav li a { + font-weight: 400; + color: #48A0D6; + padding: 6px 15px 5px 35px; +} +#docs #api-docs { + background: #FFF; + margin-left: 210px !important; + min-width: 750px; +} +#docs #api-docs #methods { + background: #292E33; + position: relative; + margin-left: 50%; +} +#docs #api-docs #methods .languages { + position: fixed; + top: 0; + width: 100%; + padding: 5px 0 5px 20px; + background: #3B4249; + box-shadow: 2px 1px 3px #000; + z-index: 999; +} +#docs #api-docs #methods .languages .language { + display: inline-block; + color: #C0CEDD; + padding: 5px 15px; + font-size: 13px; +} +#docs #api-docs #methods .languages .language.selected { + color: #fff; +} +#docs #api-docs #methods .method { + /*&:nth-child(2) {*/ + /*.method-section {*/ + /*.method-description {*/ + /*padding-top: 0px;*/ + /*border-top: 0px;*/ + /*}*/ + /*.method-example {*/ + /*padding-top: 35px;*/ + /*border-top: 0;*/ + /*box-shadow: none;*/ + /*}*/ + /*}*/ + /*}*/ +} +#docs #api-docs #methods .method:last-child .method-section { + padding-bottom: 70px; +} +#docs #api-docs #methods .method .method-section { + margin-left: -100%; + position: relative; + padding-top: 50px; +} +#docs #api-docs #methods .method .method-section .method-description { + width: 50%; + float: left; + padding: 0 30px; + border-top: 1px solid #E5E7EB; + padding-top: 40px; +} +#docs #api-docs #methods .method .method-section .method-description h3 { + font-size: 21px; + margin-top: 0; + margin-bottom: 20px; +} +#docs #api-docs #methods .method .method-section .method-description p { + font-size: 13px; + color: #666; + line-height: 21px; + margin-bottom: 20px; +} +#docs #api-docs #methods .method .method-section .method-description .info { + margin-top: 45px; + margin-bottom: 40px; +} +#docs #api-docs #methods .method .method-section .method-description .info h4 { + font-size: 14px; + border-bottom: 1px solid #E4E4E4; + padding-bottom: 15px; +} +#docs #api-docs #methods .method .method-section .method-description .info .field { + margin-top: 17px; + font-size: 13px; +} +#docs #api-docs #methods .method .method-section .method-description .info .field .key { + width: 40%; + float: left; + text-align: right; + padding-right: 10px; + font-weight: bold; + color: #333; +} +#docs #api-docs #methods .method .method-section .method-description .info .field .desc { + width: 60%; + float: right; + padding-left: 10px; + color: #555; + line-height: 21px; +} +#docs #api-docs #methods .method .method-section .method-description .info .field .desc strong { + display: block; + color: #333; + margin-bottom: 5px; +} +#docs #api-docs #methods .method .method-section .method-example { + margin-left: 50%; + width: 50%; + padding: 0px 30px 0; + border-top: 1px solid #000; + box-shadow: inset 0px 1px rgba(255, 255, 255, 0.17); + color: #fff; +} +#docs #api-docs #methods .method .method-section .method-example pre { + background: none; + border: none; + margin: 0; + padding: 20px; +} +#docs #api-docs #methods .method .method-section .method-example pre code { + background: none; + font-family: Monaco, Consolas, Menlo; + font-size: 14px; + color: #DAE4F2; + /*> * {*/ + /*color: #DAE4F2;*/ + /*line-height: 20px;*/ + /*}*/ +} +#docs #api-docs #methods .method .method-section .method-example pre code.always-visible { + display: block !important; +} +#docs #api-docs #methods .method .method-section .method-example pre code.ruby { + display: block; +} +#docs #api-docs #methods .method .method-section .method-example pre .ruby { + /*color: #DAE4F2;*/ + /*> * {*/ + /*color: #DAE4F2;*/ + /*line-height: 20px;*/ + /*}*/ + /*[class*=keyword] {*/ + /*color: #9AB4DB;*/ + /*}*/ + /*[class*=string] {*/ + /*color: #DAD0C6;*/ + /*}*/ + /*[class*=comment] {*/ + /*color: #B4B4B4;*/ + /*}*/ + /*[class*=constant] {*/ + /*color: #FFDF9D;*/ + /*}*/ + /*[class*=symbol] {*/ + /*color: #9ECBEE;*/ + /*}*/ +} +#docs #api-docs #methods .method .method-section .method-example pre .python { + color: #DAE4F2; +} +#docs #api-docs #methods .method .method-section .method-example pre .python > * { + color: #DAE4F2; + line-height: 20px; +} +#docs #api-docs #methods .method .method-section .method-example pre .python [class*=keyword] { + color: #9AB4DB; +} +#docs #api-docs #methods .method .method-section .method-example pre .python [class*=string] { + color: #DAD0C6; +} +#docs #api-docs #methods .method .method-section .method-example pre .python [class*=comment] { + color: #B4B4B4; +} +#docs #api-docs #methods .method .method-section .method-example pre .python [class*=class] { + color: #FFDF9D; +} +#docs #api-docs #methods .method .method-section .method-example pre .python [class*=params] { + color: #9ECBEE; +} +#docs #api-docs #methods .method .method-section .method-example pre .php { + color: #DAE4F2; +} +#docs #api-docs #methods .method .method-section .method-example pre .php > * { + color: #DAE4F2; + line-height: 20px; +} +#docs #api-docs #methods .method .method-section .method-example pre .php [class*=keyword] { + color: #9AB4DB; +} +#docs #api-docs #methods .method .method-section .method-example pre .php [class*=string] { + color: #DAD0C6; +} +#docs #api-docs #methods .method .method-section .method-example pre .php [class*=comment] { + color: #B4B4B4; +} + +.invoice-wrapper { + background: #eeeff1; + padding-top: 60px; +} +@media (min-width: 1200px) { + .invoice-wrapper .container { + width: 900px; + } +} +.invoice-wrapper .logo { + text-align: center; +} +.invoice-wrapper .logo a { + font-size: 36px; + color: #555; + font-weight: 300; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.invoice-wrapper .logo a:hover { + text-decoration: none; + color: #7793C4; +} +.invoice-wrapper .invoice-wrapper { + background: #FFF; + border: 1px solid #CDD3E2; + box-shadow: 0px 0px 1px #CCC; + padding: 40px 40px 60px; + margin-top: 40px; + border-radius: 4px; +} +.invoice-wrapper .invoice-wrapper .intro { + line-height: 25px; + color: #444; +} +.invoice-wrapper .invoice-wrapper .payment-info { + margin-top: 25px; + padding-top: 15px; +} +.invoice-wrapper .invoice-wrapper .payment-info span { + color: #A9B0BB; +} +.invoice-wrapper .invoice-wrapper .payment-info strong { + display: block; + color: #444; + margin-top: 3px; +} +@media (max-width: 767px) { + .invoice-wrapper .invoice-wrapper .payment-info .text-right { + text-align: left; + margin-top: 20px; + } +} +.invoice-wrapper .invoice-wrapper .payment-details { + border-top: 2px solid #EBECEE; + margin-top: 30px; + padding-top: 20px; + line-height: 22px; +} +.invoice-wrapper .invoice-wrapper .payment-details span { + color: #A9B0BB; + display: block; +} +.invoice-wrapper .invoice-wrapper .payment-details a { + display: inline-block; + margin-top: 5px; +} +@media (max-width: 767px) { + .invoice-wrapper .invoice-wrapper .payment-details .text-right { + text-align: left; + margin-top: 20px; + } +} +.invoice-wrapper .invoice-wrapper .line-items { + margin-top: 40px; +} +.invoice-wrapper .invoice-wrapper .line-items .headers { + color: #A9B0BB; + font-size: 13px; + letter-spacing: 0.3px; + border-bottom: 2px solid #EBECEE; + padding-bottom: 4px; +} +.invoice-wrapper .invoice-wrapper .line-items .items { + margin-top: 8px; + border-bottom: 2px solid #EBECEE; + padding-bottom: 8px; +} +.invoice-wrapper .invoice-wrapper .line-items .items .item { + padding: 10px 0; + color: #696969; + font-size: 15px; +} +@media (max-width: 767px) { + .invoice-wrapper .invoice-wrapper .line-items .items .item { + font-size: 13px; + } +} +.invoice-wrapper .invoice-wrapper .line-items .items .item .amount { + letter-spacing: 0.1px; + color: #84868A; + font-size: 16px; +} +@media (max-width: 767px) { + .invoice-wrapper .invoice-wrapper .line-items .items .item .amount { + font-size: 13px; + } +} +.invoice-wrapper .invoice-wrapper .line-items .total { + margin-top: 30px; +} +.invoice-wrapper .invoice-wrapper .line-items .total .extra-notes { + float: left; + width: 40%; + text-align: left; + font-size: 13px; + color: #7A7A7A; + line-height: 20px; +} +@media (max-width: 767px) { + .invoice-wrapper .invoice-wrapper .line-items .total .extra-notes { + width: 100%; + margin-bottom: 30px; + float: none; + } +} +.invoice-wrapper .invoice-wrapper .line-items .total .extra-notes strong { + display: block; + margin-bottom: 5px; + color: #454545; +} +.invoice-wrapper .invoice-wrapper .line-items .total .field { + margin-bottom: 7px; + font-size: 14px; + color: #555; +} +.invoice-wrapper .invoice-wrapper .line-items .total .field.grand-total { + margin-top: 10px; + font-size: 16px; + font-weight: 500; +} +.invoice-wrapper .invoice-wrapper .line-items .total .field.grand-total span { + color: #20A720; + font-size: 16px; +} +.invoice-wrapper .invoice-wrapper .line-items .total .field span { + display: inline-block; + margin-left: 20px; + min-width: 85px; + color: #84868A; + font-size: 15px; +} +.invoice-wrapper .invoice-wrapper .line-items .print { + margin-top: 50px; + text-align: center; +} +.invoice-wrapper .invoice-wrapper .line-items .print a { + display: inline-block; + border: 1px solid #9CB5D6; + padding: 13px 13px; + border-radius: 5px; + color: #708DC0; + font-size: 13px; + -webkit-transition: all 0.2s linear; + -moz-transition: all 0.2s linear; + -ms-transition: all 0.2s linear; + -o-transition: all 0.2s linear; + transition: all 0.2s linear; +} +.invoice-wrapper .invoice-wrapper .line-items .print a:hover { + text-decoration: none; + border-color: #333; + color: #333; +} +.invoice-wrapper .invoice-wrapper .line-items .print a i { + margin-right: 3px; + font-size: 14px; +} +.invoice-wrapper .footer { + margin-top: 40px; + margin-bottom: 110px; + text-align: center; + font-size: 12px; + color: #969CAD; +} + +.timeline-bg { + background: #f7f8fa; +} + +.timeline-wrapper { + margin-top: 60px; + position: relative; + /* the first node that comes right next to a year */ +} +.timeline-wrapper:before { + content: ""; + position: absolute; + width: 5px; + height: 100%; + left: 0; + background: #e7e8ec; + right: 0; + margin: 0 auto; + z-index: -1; +} +.timeline-wrapper .year { + text-align: center; + margin-bottom: 50px; + font-weight: 600; + margin-top: 50px; + font-size: 16px; +} +.timeline-wrapper .year:nth-child(1) { + margin-top: 0px; + margin-bottom: 60px; +} +.timeline-wrapper .year.last { + margin-bottom: 0; + font-size: 21px; +} +.timeline-wrapper .year span { + display: inline-block; + background: #E7E8EC; + padding: 6px 20px; + color: #5A5A5A; + border-radius: 3px; +} +.timeline-wrapper .year + .node { + margin-top: -20px; +} +.timeline-wrapper .node { + position: relative; + float: right; + clear: right; + width: 44%; + margin: 35px 0; + border-radius: 5px; +} +@media (max-width: 767px) { + .timeline-wrapper .node { + width: 100%; + } +} +.timeline-wrapper .node.left { + float: left; + clear: left; +} +.timeline-wrapper .node.left:before { + right: -51px; + left: inherit; +} +@media (max-width: 991px) { + .timeline-wrapper .node.left:before { + right: -42px; + width: 43px; + } +} +.timeline-wrapper .node.left .marker { + left: inherit; + right: -65px; +} +@media (max-width: 991px) { + .timeline-wrapper .node.left .marker { + right: -52px; + } +} +.timeline-wrapper .node:before { + content: ""; + position: absolute; + width: 50px; + height: 4px; + background: #E7E8EC; + left: -51px; + top: 62px; +} +@media (max-width: 991px) { + .timeline-wrapper .node:before { + left: -42px; + width: 43px; + } +} +@media (max-width: 767px) { + .timeline-wrapper .node:before { + display: none; + } +} +.timeline-wrapper .node .marker { + position: absolute; + width: 17px; + height: 17px; + border-radius: 25px; + background: #e7e8ec; + left: -65px; + top: 55px; + border: 3px solid #F7F8FA; +} +@media (max-width: 991px) { + .timeline-wrapper .node .marker { + left: -51px; + } +} +@media (max-width: 767px) { + .timeline-wrapper .node .marker { + display: none; + } +} +.timeline-wrapper .node .entry { + position: relative; + padding: 20px; + height: 400px; + background-size: cover; + background-position: center center; + border-radius: 5px; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.26); +} +.timeline-wrapper .node .entry:before { + position: absolute; + top: 50%; + bottom: 0; + left: 0; + right: 0; + content: ""; + border-radius: 0 0 5px 5px; + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.89) 100%); + background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.89) 100%); + background: -o-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.89) 100%); + background: linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.89) 100%); +} +.timeline-wrapper .node .entry.smallish { + height: 320px; +} +.timeline-wrapper .node .entry.smallish .intro { + top: 68%; +} +.timeline-wrapper .node .entry.short-entry { + background: #E7E8EC; + color: #5c6269; + height: inherit; + padding: 13px; + box-shadow: none; + position: relative; + top: 41px; + left: -1px; + margin-bottom: 20px; + text-shadow: 1px 1px rgba(255, 255, 255, 0.4); +} +.timeline-wrapper .node .entry.short-entry:before { + background: none; +} +.timeline-wrapper .node .entry .intro { + position: relative; + top: 75%; + color: #FFF; + z-index: 9; + letter-spacing: 0.3px; +} +.timeline-wrapper .node .entry .intro h4 { + font-size: 20px; + text-shadow: 1px 1px rgba(0, 0, 0, 0.69); + letter-spacing: 0.5px; +} +@media (max-width: 767px) { + .timeline-wrapper .node .entry .intro h4 { + font-size: 17px; + } +} +.timeline-wrapper .node .entry .intro p { + font-weight: 300; +} +@media (max-width: 767px) { + .timeline-wrapper .node .entry .intro p { + font-size: 13px; + } +} +.timeline-wrapper .node .news { + position: relative; + background-size: cover; + background-position: center center; + border-radius: 5px; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.26); + background: #fff; +} +.timeline-wrapper .node .news section { + padding: 30px 20px 60px 20px; +} +.timeline-wrapper .node .news section h3 { + margin-top: 0; + font-size: 17px; + line-height: 22px; +} +.timeline-wrapper .node .news section p { + margin-top: 21px; + line-height: 22px; + font-size: 13px; + color: #7A7979; +} +.timeline-wrapper .node .news footer { + position: absolute; + bottom: 0; + padding: 12px 20px; + width: 100%; + font-size: 12px; + font-weight: 500; + color: #9EAEBE; +} +.timeline-wrapper .node .news footer .tag { + position: relative; + padding-left: 27px; + text-transform: uppercase; +} +.timeline-wrapper .node .news footer .tag span { + font-size: 24px; + position: absolute; + top: -4px; + left: 0; +} + +#docs #guide { + margin-top: 70px; +} +#docs #api-docs { + margin-left: 200px !important; + margin-top: -20px !important; +} + +.index-sidebar-hero .cta a.button { + padding: 12px 30px; +} + +a.anchor { + display: block; + position: relative; + visibility: hidden; +} +@media (min-width: 1px) { + a.anchor { + top: -80px; + } +} +@media (min-width: 768px) { + a.anchor { + top: -140px; + } +} + +.table-small { + font-size: 80%; +} + +a.header:hover { + text-decoration: none !important; +} + +@media (min-width: 1px) { + .navbar.normal { + min-height: 75px; + max-height: 75px; + margin-bottom: 75px; + } + .navbar.normal .navbar-brand img { + width: 130px; + height: 130px; + } + .navbar.normal .navbar-nav { + font-size: 22px; + position: relative; + bottom: 15px; + } + .navbar.normal .nav-links { + left: 140px; + } + .navbar.normal .nav-links .quickstart { + display: none; + } + .navbar.normal .nav-links .tutorial { + display: none; + } + .navbar.normal .nav li { + display: inline-block; + } + .navbar.normal .nav .li a { + padding: 10px 5px; + } +} +@media (min-width: 768px) { + .navbar.normal { + min-height: 125px; + margin-bottom: 125px; + } + .navbar.normal .navbar-brand img { + width: 200px; + height: 200px; + } + .navbar.normal .nav.gh { + margin-top: 50px; + } + .navbar.normal .nav .li a { + padding: 10px 5px; + } + .navbar.normal .nav-links { + font-size: 32px; + left: 190px; + top: 10px; + } + .navbar.normal .nav-links .quickstart { + display: inline-block; + } + .navbar.normal .nav-links .tutorial { + display: inline-block; + } +} +.guides-video iframe { + width: 100%; + margin: 0 auto; +} + +.navbar.normal { + position: fixed; + width: 100%; + /*background: url("https://www.toptal.com/designers/subtlepatterns/patterns/connectwork.png");*/ + background: url("/1.13/assets/img/fancy-cushion.png"); + /*background: url("https://www.toptal.com/designers/subtlepatterns/patterns/dark-honeycomb.png");*/ + border-bottom: 1px solid black; + box-shadow: 1px 1px 15px -2px black; + /*background: black;*/ +} +.navbar.normal .navbar-header { + margin-top: -50px; +} +.navbar.normal .navbar-nav { + margin-top: 30px; + font-family: "Boogaloo", cursive; + font-weight: bold; +} +.navbar.normal .navbar-nav a { + color: white !important; +} +.navbar.normal .navbar-brand img { + transition: all 200ms; +} +.navbar.normal .navbar-brand img:hover { + transform: rotate(-10deg); +} +.navbar.normal .star { + position: absolute; + bottom: 0px; + right: 65px; + font-size: 26px; +} + +strong { + font-weight: bold; + text-shadow: 2px 2px black; + color: white; +} + +.nav-menu .main-menu h4 { + color: rgb(229.5, 229.5, 229.5); + margin-left: 5rem; +} +.nav-menu .main-menu ul { + margin-top: 2rem; +} +.nav-menu .main-menu ul li a { + padding-left: 3rem; +} + +.spacer { + width: 100%; + margin-bottom: 25px; +} + +img.drop { + box-shadow: 2px 2px 6px 1px rgba(0, 0, 0, 0.3); +} + +blockquote { + border: 1px solid black !important; +} + +.layout-home .brand { + display: block; + margin-left: auto; + margin-right: auto; + width: 300px; + text-align: center; + padding-top: 200px; +} +.layout-home .brand .graphiti { + font-size: 50px; +} +.layout-home .brand .desc { + color: #b0b0b0; +} + +.tutorial ul { + padding-left: 20px; +} + +body { + background: #414B5B; + font-size: 17px; + line-height: 1.7; + color: rgb(76.5, 76.5, 76.5); +} +body.layout-home { + background-image: url("/1.13/assets/img/home-bg.jpg"); + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + /*background-size: cover;*/ +} +body.layout-home .holder { + position: absolute; + top: 20%; + left: 50%; + width: 500px; + height: 500px; + margin-top: -250px; /* Half the height */ + margin-left: -250px; + transition: 200ms all; +} +body.layout-home .holder:hover { + /*margin-top: -260px;*/ +} +body.layout-home .holder a:hover { + text-decoration: none; +} +body.layout-home .holder h1 { + margin-top: -75px; +} +body:not(.layout-home) .page-content { + background-image: url("https://images.unsplash.com/photo-1487452066049-a710f7296400?ixlib=rb-1.2.1&auto=format&fit=crop&w=2102&q=80"); + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + /*:not(.twitter-tweet) {*/ + /*text-shadow: 1px 1px black;*/ + /*font-family: 'Noto Sans', sans-serif;*/ + /*}*/ +} +body:not(.layout-home) .page-content .wrapper > div.container { + background: url("https://storage.googleapis.com/subtlepatterns-production/designers/subtlepatterns/uploads/dark-honeycomb.png"); + /*background: #5c666f;*/ + border: 1px solid black; + color: rgb(233.75, 233.75, 233.75); + font-size: 18px; + border-radius: 5px; + border: 1px solid black; + box-shadow: -2px 2px 6px 2px rgba(0, 0, 0, 0.4); +} +@media (min-width: 1px) { + body:not(.layout-home) .page-content .wrapper > div.container { + padding-top: 80px; + } +} +@media (min-width: 768px) { + body:not(.layout-home) .page-content .wrapper > div.container { + padding-top: 150px; + } +} +body:not(.layout-home) .page-content .wrapper > div.container img { + border-radius: 5px; + border: 1px solid black; + box-shadow: 2px 2px 6px 1px rgba(0, 0, 0, 0.3); +} + +#next { + float: right; + margin: 40px 0 40px 0; +} + +h1 { + font-size: 40px; +} + +h2 { + font-size: 36px; +} + +h1, h2 { + font-family: "Boogaloo", cursive; + color: white; + text-shadow: 4px 4px black; +} +h1 a, h1 a:hover, h2 a, h2 a:hover { + color: black; + text-decoration: none; + position: relative; +} +h1 a:hover:before, h2 a:hover:before { + content: "¶"; + position: absolute; + left: -18px; + color: grey; + font-size: 18px; + font-family: sans-serif; + top: 4px; +} + +h3, h4, h5 { + font-family: "Boogaloo", cursive; + color: white; +} +h3 a, h3 a:hover, h4 a, h4 a:hover, h5 a, h5 a:hover { + color: white; + text-decoration: none; +} + +h5 { + color: white; + font-size: 17px; + margin-top: 30px; + border-bottom: 1px solid #f0f0f0; +} + +h4 { + color: white; + margin-top: 40px; + border-bottom: 1px solid rgb(214.5, 214.5, 214.5); +} + +.main-footer { + background: url("https://www.toptal.com/designers/subtlepatterns/patterns/dark-honeycomb.png"); + margin-top: 0; + bottom: 0; +} + +.get-started { + margin-top: 40px; + padding: 40px 20px; + background: -webkit-linear-gradient(top, #355069 0, #5E7A9B 100%); + /*background: radial-gradient(#36414E, #253241);*/ + font-size: 16px; + color: #f8f8f8; + font-weight: 400; + line-height: 25px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} +.get-started h2 { + color: white; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} +.get-started a { + color: #90CAF9; + font-weight: bold; +} + +#docs #api-docs #methods .method .method-section { + padding-top: 0; +} + +#cover-image.prod-bg { + background-image: url("/1.13/assets/img/sunrise.png") !important; +} +#cover-image.dev-bg { + background-image: url("/1.13/assets/img/sunrise.png") !important; +} + +.solution img { + margin-left: 30px !important; + height: 300px; +} + +code span { + font-family: "Roboto Mono", sans-serif !important; + text-shadow: none !important; +} + +pre { + background: #292E33; + border: 1px solid black; +} +pre code { + font-family: "Roboto Mono", sans-serif; + text-shadow: none !important; + font-size: 15px !important; +} +pre code.hljs { + background: #292E33; + color: rgb(236.87, 241.77, 248.63); + line-height: 20px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code.highlighter-rouge { + background: rgb(29.6358695652, 33.25, 36.8641304348); + color: #b3ffb3; + /*color: lighten(#c7254e, 10%);*/ +} + +a, a:not([href]):not([tabindex]) { + color: #90CAF9; + cursor: pointer; + font-weight: bold; + text-shadow: 1px 1px black; +} +a:hover, a:not([href]):not([tabindex]):hover { + color: rgb(192.3846153846, 225.1025641026, 251.6153846154); +} + +img[alt=github] { + margin-right: 5px; + width: 20px; + height: 20px; +} + +.toc { + font-size: 15px; + line-height: 1.8; +} +.toc > ul { + padding-left: 10px; +} + +.js-toc, .ruby-toc, blockquote { + background: #292E33; + /*background: #f7f8fb;*/ + border: 1px solid black; + border-radius: 5px; + padding: 10px; + line-height: 1.8; +} + +.ruby-toc ul { + padding-left: 25px; +} + +.ruby-toc + div { + margin-bottom: 20px; + font-size: 16px; +} + +blockquote.url { + font-size: 14px; + color: grey; + margin-top: -12px; +} + +ul ul { + padding-left: 20px; +} + +pre.highlight code.coffeescript [class*=built_in], pre.highlight code.cs [class*=built_in], pre.highlight code.javascript [class*=built_in], pre.highlight code.js [class*=built_in], pre.highlight code.ts [class*=built_in], pre.highlight code.php [class*=built_in], pre.hljs code.coffeescript [class*=built_in], pre.hljs code.cs [class*=built_in], pre.hljs code.javascript [class*=built_in], pre.hljs code.js [class*=built_in], pre.hljs code.ts [class*=built_in], pre.hljs code.php [class*=built_in] { + color: rgb(172.799270073, 193.9598540146, 225.700729927); +} +pre.highlight code.coffeescript [class*=string], pre.highlight code.cs [class*=string], pre.highlight code.javascript [class*=string], pre.highlight code.js [class*=string], pre.highlight code.ts [class*=string], pre.highlight code.php [class*=string], pre.hljs code.coffeescript [class*=string], pre.hljs code.cs [class*=string], pre.hljs code.javascript [class*=string], pre.hljs code.js [class*=string], pre.hljs code.ts [class*=string], pre.hljs code.php [class*=string] { + color: lightgreen; +} +pre.highlight code.coffeescript [class*=attribute], pre.highlight code.cs [class*=attribute], pre.highlight code.javascript [class*=attribute], pre.highlight code.js [class*=attribute], pre.highlight code.ts [class*=attribute], pre.highlight code.php [class*=attribute], pre.hljs code.coffeescript [class*=attribute], pre.hljs code.cs [class*=attribute], pre.hljs code.javascript [class*=attribute], pre.hljs code.js [class*=attribute], pre.hljs code.ts [class*=attribute], pre.hljs code.php [class*=attribute] { + color: white; +} +pre.highlight code.coffeescript [class*=keyword], pre.highlight code.cs [class*=keyword], pre.highlight code.javascript [class*=keyword], pre.highlight code.js [class*=keyword], pre.highlight code.ts [class*=keyword], pre.highlight code.php [class*=keyword], pre.hljs code.coffeescript [class*=keyword], pre.hljs code.cs [class*=keyword], pre.hljs code.javascript [class*=keyword], pre.hljs code.js [class*=keyword], pre.hljs code.ts [class*=keyword], pre.hljs code.php [class*=keyword] { + color: rgb(224.5652173913, 135.4347826087, 135.4347826087); + font-weight: 600; +} +pre.highlight code.coffeescript [class*=literal], pre.highlight code.cs [class*=literal], pre.highlight code.javascript [class*=literal], pre.highlight code.js [class*=literal], pre.highlight code.ts [class*=literal], pre.highlight code.php [class*=literal], pre.hljs code.coffeescript [class*=literal], pre.hljs code.cs [class*=literal], pre.hljs code.javascript [class*=literal], pre.hljs code.js [class*=literal], pre.hljs code.ts [class*=literal], pre.hljs code.php [class*=literal] { + color: #ff9999; +} +pre.highlight code.coffeescript [class*=c1], pre.highlight code.cs [class*=c1], pre.highlight code.javascript [class*=c1], pre.highlight code.js [class*=c1], pre.highlight code.ts [class*=c1], pre.highlight code.php [class*=c1], pre.hljs code.coffeescript [class*=c1], pre.hljs code.cs [class*=c1], pre.hljs code.javascript [class*=c1], pre.hljs code.js [class*=c1], pre.hljs code.ts [class*=c1], pre.hljs code.php [class*=c1] { + color: #B4B4B4; +} +pre.highlight code.coffeescript [class*=constant], pre.highlight code.cs [class*=constant], pre.highlight code.javascript [class*=constant], pre.highlight code.js [class*=constant], pre.highlight code.ts [class*=constant], pre.highlight code.php [class*=constant], pre.hljs code.coffeescript [class*=constant], pre.hljs code.cs [class*=constant], pre.hljs code.javascript [class*=constant], pre.hljs code.js [class*=constant], pre.hljs code.ts [class*=constant], pre.hljs code.php [class*=constant] { + color: #FFDF9D; +} +pre.highlight code.coffeescript [class*=nx], pre.highlight code.cs [class*=nx], pre.highlight code.javascript [class*=nx], pre.highlight code.js [class*=nx], pre.highlight code.ts [class*=nx], pre.highlight code.php [class*=nx], pre.hljs code.coffeescript [class*=nx], pre.hljs code.cs [class*=nx], pre.hljs code.javascript [class*=nx], pre.hljs code.js [class*=nx], pre.hljs code.ts [class*=nx], pre.hljs code.php [class*=nx] { + color: rgb(146.6, 204.425, 249.4); + font-weight: 600; +} +pre.highlight code.typescript [class*=kr], pre.hljs code.typescript [class*=kr] { + color: rgb(216.75, 216.75, 38.25); +} +pre.highlight code.ruby .class .title, pre.highlight code.ruby .class .parent, pre.hljs code.ruby .class .title, pre.hljs code.ruby .class .parent { + color: #FFDF9D; +} +pre.highlight code.ruby [class*=keyword], pre.hljs code.ruby [class*=keyword] { + color: rgb(255, 127.5, 127.5); +} +pre.highlight code.ruby [class*=kp], pre.hljs code.ruby [class*=kp] { + color: rgb(255, 127.5, 127.5); +} +pre.highlight code.ruby [class*=string], pre.hljs code.ruby [class*=string] { + color: lightgreen; +} +pre.highlight code.ruby [class*=comment], pre.hljs code.ruby [class*=comment] { + color: #B4B4B4; +} +pre.highlight code.ruby [class*=constant], pre.hljs code.ruby [class*=constant] { + color: #FFDF9D; +} +pre.highlight code.ruby [class*=symbol], pre.hljs code.ruby [class*=symbol] { + color: #9ECBEE; +} + +img[alt=glimmer_logo] { + margin: 0 auto; + width: 200px; + display: block; + margin-bottom: 4em; +} + +.code-tabs .highlight { + margin-top: 0; +} + +.tabs { + /*background: black;*/ + border-radius: 3px; +} +.tabs .tab { + background-color: #292E33; + border: 1px solid black; + font-weight: 500; + color: #888888; + display: inline-block; + padding: 0 8px 0 8px; + margin-top: 10px; + cursor: pointer; + -webkit-transition: background-color 0.2s linear; + -moz-transition: background-color 0.2s linear; + -ms-transition: background-color 0.2s linear; + -o-transition: background-color 0.2s linear; + transition: background-color 0.2s linear; + border-bottom: 1px solid black; + font-size: 14px; +} +.tabs .tab.active { + color: white; + text-shadow: 2px 2px black; + font-weight: bold; + /*background-color: #fff;*/ + border-radius: 5px 5px 0 0; +} + +#next a, #next a small { + color: white; +} + +.highlight { + margin-top: 2rem; + margin-bottom: 2rem; +} +.highlight pre code { + font-size: 14px; + line-height: 1.5; +} +.highlight .language-ruby .n { + color: white; +} +.highlight .language-ruby .c1 { + color: #9c9c9c; +} +.highlight .language-ruby .ss { + color: #a2dcff; + font-weight: 600; +} +.highlight .language-ruby .k, .highlight .language-ruby .kp { + color: #ff8080; +} +.highlight .language-ruby .p { + color: white; +} +.highlight .language-ruby .nc, .highlight .language-ruby .no { + color: #e6eb9d; + font-weight: 600; +} +.highlight .language-ruby .o, .highlight .language-ruby .si, .highlight .language-ruby .nf { + color: white; +} +.highlight .language-ruby .s1, .highlight .language-ruby .s2, .highlight .language-ruby .dl { + color: lightgreen; + font-weight: 600; +} +.highlight .language-ruby .vi, .highlight .language-ruby .mi, .highlight .language-ruby .nb { + color: rgb(255, 153, 229.6666666667); + font-weight: 600; +} +.highlight .language-http { + color: #edf2f9; +} +.highlight .language-typescript .kd, .highlight .language-javascript .kd { + color: #edf2f9; + font-weight: 600; +} +.highlight .language-typescript .o, .highlight .language-javascript .o { + color: #edf2f9; +} +.highlight .language-typescript .p, .highlight .language-javascript .p { + color: #edf2f9; +} +.highlight .language-typescript .nx, .highlight .language-typescript .nd, .highlight .language-javascript .nx, .highlight .language-javascript .nd { + color: #a2dcff; + font-weight: 600; +} +.highlight .language-typescript .nb, .highlight .language-javascript .nb { + color: white; +} +.highlight .language-typescript .kc, .highlight .language-javascript .kc { + color: #e6eb9d; +} +.highlight .language-typescript .kr, .highlight .language-javascript .kr { + color: #00e6e6; +} +.highlight .language-typescript .c1, .highlight .language-javascript .c1 { + color: #9c9c9c; +} +.highlight .language-typescript .k, .highlight .language-javascript .k { + color: #ff66d9; +} +.highlight .language-typescript .err, .highlight .language-javascript .err { + font-weight: 600; + color: #e6eb9d; +} +.highlight .language-typescript .na, .highlight .language-typescript .nl, .highlight .language-javascript .na, .highlight .language-javascript .nl { + color: #ff66d9; +} +.highlight .language-typescript .s1, .highlight .language-typescript .s2, .highlight .language-typescript .dl, .highlight .language-javascript .s1, .highlight .language-javascript .s2, .highlight .language-javascript .dl { + color: lightgreen; + font-weight: 600; +} +.highlight .language-typescript .mi, .highlight .language-javascript .mi { + color: #e6eb9d; +} +.highlight .language-error { + color: #ff6666; + font-weight: 600; +} +.highlight .language-bash { + color: white; +} +.highlight .language-javascript .k { + color: #e6eb9d !important; +} + +.home { + width: 95%; + margin: 0 auto; + text-align: center; + font-size: 16px; + line-height: 1.4; + padding-top: 10px; + z-index: 20; +} +@media (min-width: 569px) { + .home { + font-size: 18px; + } +} +.home .skip-intro { + position: absolute; + right: 20px; + padding: 3px 6px; + font-size: 90%; +} +.home .start { + margin-top: 40px; +} +.home .img { + position: absolute; + background-repeat: no-repeat; + opacity: 0.1; + width: 100%; + height: 100%; + background-position: 70% 10%; + background-size: 40%; +} +.home .sample .highlight pre code { + font-size: 14px !important; +} +@media (min-width: 569px) { + .home .sample .highlight pre code { + font-size: 18px !important; + } +} +.home .sample.s .highlight pre code { + font-size: 11px !important; +} +@media (min-width: 569px) { + .home .sample.s .highlight pre code { + font-size: 18px !important; + } +} +.home .sample figure:last-child { + margin-bottom: 18px; +} +.home .mag { + color: rgb(229.5, 0, 229.5); +} +.home .lg { + color: grey; +} +.home .p { + margin-bottom: 2.5rem; +} +.home .bold { + font-weight: bold; +} +.home .italic { + font-style: italic; +} +.home .underline { + text-decoration: underline; +} +.home .s { + font-size: 19.2px; +} +.home .m { + font-size: 25.6px; +} +.home .l { + font-size: 32px; +} +.home .xl { + font-size: 35.2px; +} +.home .quote { + margin-top: 5px; + font-size: 19.2px; +} +@media (min-width: 569px) { + .home .quote { + font-size: 24px !important; + } +} +@media (min-width: 569px) { + .home .quote.yehuda { + font-size: 30px !important; + } +} +.home .highlight { + text-align: left; +} +.home .highlight pre { + padding-bottom: 0; +} + +.blog h2 a { + color: white; +} +.blog h2 a:hover:before { + display: none; +} + +/*# sourceMappingURL=main.css.map */ \ No newline at end of file diff --git a/website/static/1.13/assets/main.css.map b/website/static/1.13/assets/main.css.map new file mode 100644 index 00000000..c473f3c5 --- /dev/null +++ b/website/static/1.13/assets/main.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../_sass/modules/_mixins.scss","../_sass/partials/_config.scss","../_sass/modules/_elements.scss","../_sass/modules/_buttons.scss","../_sass/theme.scss","../_sass/bootstrap/bootstrap.scss","../_sass/bootstrap/_overrides.scss","../_sass/partials/_layout.scss","../_sass/partials/snippets/_features.scss","../_sass/partials/snippets/_slider.scss","../_sass/partials/pages/_index.scss","../_sass/partials/pages/_index2.scss","../_sass/partials/pages/_index3.scss","../_sass/partials/pages/_features.scss","../_sass/partials/pages/_services.scss","../_sass/partials/pages/_signup.scss","../_sass/partials/pages/_signup_rotate.scss","../_sass/partials/pages/_blog.scss","../_sass/partials/pages/_blog_cols.scss","../_sass/partials/pages/_blog_timeline.scss","../_sass/partials/pages/_blogpost.scss","../_sass/partials/pages/_pricing.scss","../_sass/partials/pages/_charts.scss","../_sass/partials/pages/_aboutus.scss","../_sass/partials/pages/_contactus.scss","../_sass/partials/pages/_status.scss","../_sass/partials/pages/_gallery.scss","../_sass/partials/pages/_portfolio.scss","../_sass/partials/pages/_portfolio_item.scss","../_sass/partials/pages/_support.scss","../_sass/partials/pages/_404.scss","../_sass/partials/pages/_coming_soon.scss","../_sass/partials/pages/_docs.scss","../_sass/partials/pages/_invoice.scss","../_sass/partials/pages/_timeline.scss"],"names":[],"mappings":";AAkBA;AACA;AClBQ;AAEA;AAEA;AAEA;AAEA;ACTR;EACI;;;AAGJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EFaA;EACA;EACA;EACA;EACA;EAtBA,oBEQoB;EFPlB,iBEOkB;EFNlB,gBEMkB;EFLlB,eEKkB;EFJlB,YEIkB;;AAEpB;EACE;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EFtCA,oBEwCoB;EFvClB,iBEuCkB;EFtClB,gBEsCkB;EFrClB,eEqCkB;EFpClB,YEoCkB;;AAEpB;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EAEE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAMN;AAME;AAAA;AAAA;EACE;EACA;EACA;EACA;;AFzGF;AAAA;AAAA;EE4GI;EACA;EACA;;AF7GJ;AAAA;AAAA;EE2GI;EACA;EACA;;AF5GJ;AAAA;AAAA;EE0GI;EACA;EACA;;AF3GJ;AAAA;AAAA;EEyGI;EACA;EACA;;AAEF;AAAA;AAAA;EACE;EACA;EACA;;;AAMJ;EACE;EACA;EACA;;AF5HF;EE+HI;EACA;EACA;;AFhIJ;EE8HI;EACA;EACA;;AF/HJ;EE6HI;EACA;EACA;;AF9HJ;EE4HI;EACA;EACA;;AAGF;EACE;EACA;EACA;;;ACpLN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EHAA,oBGEoB;EHDlB,iBGCkB;EHAlB;EACA,eGDkB;EHElB,YGFkB;AAYpB;AAYA;;AAtBA;EAEE;EACA;;AAGF;EACE;;AAKF;EACE;EACA;;AAGF;EACE;EACA;;AAKF;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;;AC1DJ;ACJA;AAAA;AAAA;AAAA;AAAA;AAKA;AACA;EACE;EACA;EACI;;;AAEN;EACE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAaE;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACG;EACK;;;AAEV;EACE;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;EAKE;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACG;EACK;EACR;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACG;EACK;EACR;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AACA;EACE;AAAA;AAAA;IAGE;IACA;IACA;IACA;IACQ;;EAEV;AAAA;IAEE;;EAEF;IACE;;EAEF;IACE;;EAEF;AAAA;IAEE;;EAEF;AAAA;IAEE;IAEA;;EAEF;IACE;;EAEF;AAAA;IAEE;;EAEF;IACE;;EAEF;AAAA;AAAA;IAGE;IACA;;EAEF;AAAA;IAEE;;EAEF;IACE;;EAEF;AAAA;IAEE;;EAEF;IACE;;EAEF;IACE;;EAEF;AAAA;IAEE;;EAEF;AAAA;IAEE;;;AAGJ;EACE;EAEA;EACA;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACG;EACK;;;AAEV;AAAA;EAEE;EACG;EACK;;;AAEV;EACE;EAEA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;AAAA;AAAA;AAAA;EAKE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACK;EACG;;;AAEV;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAYE;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAwBE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAYE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAYE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;IACE;;;AAGJ;AAAA;EAEE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;AAAA;AAAA;EAIE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;EAEF;IACE;;;AAGJ;AAAA;EAEE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;EAIE;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACQ;;;AAEV;EACE;EACA;EACA;EACA;EACQ;;;AAEV;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;;AAGJ;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;;AAGJ;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;IACE;;;AAGJ;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAEF;EACE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAYE;;;AAEF;AAAA;AAAA;AAAA;AAAA;EAKE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAYE;;;AAEF;AAAA;AAAA;AAAA;AAAA;EAKE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAYE;;;AAEF;AAAA;AAAA;AAAA;AAAA;EAKE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAYE;;;AAEF;AAAA;AAAA;AAAA;AAAA;EAKE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAYE;;;AAEF;AAAA;AAAA;AAAA;AAAA;EAKE;;;AAEF;EACE;EACA;;;AAEF;EACE;IACE;IACA;IACA;IACA;IACA;;EAEF;IACE;;EAEF;AAAA;AAAA;AAAA;AAAA;AAAA;IAME;;EAEF;IACE;;EAEF;AAAA;AAAA;AAAA;AAAA;AAAA;IAME;;EAEF;AAAA;AAAA;AAAA;AAAA;AAAA;IAME;;EAEF;AAAA;AAAA;AAAA;IAIE;;;AAGJ;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACG;EACK;;;AAEV;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACQ;EACR;EACK;EACG;;;AAEV;EACE;EACA;EACA;EACQ;;;AAEV;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;AAAA;AAAA;AAAA;IAIE;;EAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;IAQE;;EAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;IAQE;;;AAGJ;EACE;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAEF;AAAA;AAAA;AAAA;EAIE;;;AAEF;AAAA;AAAA;AAAA;EAIE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAUE;;;AAEF;EACE;EACA;EACQ;;;AAEV;EACE;EACA;EACQ;;;AAEV;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAUE;;;AAEF;EACE;EACA;EACQ;;;AAEV;EACE;EACA;EACQ;;;AAEV;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAUE;;;AAEF;EACE;EACA;EACQ;;;AAEV;EACE;EACA;EACQ;;;AAEV;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;;EAEF;IACE;IACA;;EAEF;AAAA;AAAA;IAGE;;EAEF;IACE;;EAEF;IACE;IACA;;EAEF;AAAA;IAEE;IACA;IACA;IACA;;EAEF;AAAA;IAEE;;EAEF;AAAA;IAEE;IACA;;EAEF;IACE;;;AAGJ;AAAA;AAAA;AAAA;EAIE;EACA;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;;;AAEF;EACE;IACE;IACA;IACA;;;AAGJ;EACE;;;AAEF;EACE;IACE;IACA;;;AAGJ;EACE;IACE;IACA;;;AAGJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACI;EACJ;EACA;EACG;EACC;EACI;EACR;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACQ;;;AAEV;AAAA;AAAA;EAGE;EACA;EACA;EACQ;EACR;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;EAKE;EACA;EACQ;;;AAEV;AAAA;AAAA;AAAA;EAIE;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;EACE;EACA;EACK;EACG;;;AAEV;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACK;EACG;EACR;EACK;EACG;EACR;EACK;EACG;;;AAEV;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACQ;EACR;EACA;EACA;EACA;EACQ;;;AAEV;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;IACE;IACA;;EAEF;IACE;IACA;;;AAGJ;AAAA;EAEE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQE;;;AAEF;AAAA;AAAA;AAAA;EAIE;;;AAEF;EACE;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACQ;;;AAEV;EACE;EACQ;;;AAEV;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAOE;EACA;;;AAEF;EACE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAOE;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;IACE;IACA;;EAEF;IACE;;;AAGJ;EACE;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;EACE;IACE;IACA;;EAEF;AAAA;AAAA;IAGE;;;AAGJ;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;IACE;IACA;;EAEF;IACE;;;AAGJ;EACE;;;AAEF;EACE;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;EACE;IACE;IACA;;EAEF;AAAA;AAAA;IAGE;;;AAGJ;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;EACA;EACA;EACA;EACA;EACA;EACQ;;;AAEV;EACE;;;AAEF;EACE;IACE;IACA;IACA;IACQ;;EAEV;IACE;IACA;IACA;IACA;;EAEF;IACE;;EAEF;AAAA;AAAA;IAGE;IACA;;;AAGJ;AAAA;EAEE;;;AAEF;EACE;AAAA;IAEE;;;AAGJ;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;EACE;AAAA;AAAA;AAAA;IAIE;IACA;;;AAGJ;EACE;EACA;;;AAEF;EACE;IACE;;;AAGJ;AAAA;EAEE;EACA;EACA;EACA;;;AAEF;EACE;AAAA;IAEE;;;AAGJ;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;EACE;AAAA;IAEE;;;AAGJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;IACE;;;AAGJ;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;IACE;IACA;IACA;IACA;IACA;IACA;IACA;IACQ;;EAEV;AAAA;IAEE;;EAEF;IACE;;EAEF;AAAA;IAEE;;;AAGJ;EACE;IACE;IACA;;EAEF;IACE;;EAEF;IACE;IACA;;;AAGJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACQ;;;AAEV;EACE;IACE;IACA;IACA;;EAEF;IACE;IACA;IACA;;EAEF;IACE;;EAEF;IACE;IACA;;EAEF;AAAA;AAAA;IAGE;;EAEF;IACE;;EAEF;IACE;IACA;;EAEF;AAAA;IAEE;IACA;IACA;IACA;;EAEF;AAAA;IAEE;;EAEF;AAAA;IAEE;IACA;;EAEF;IACE;;;AAGJ;EACE;IACE;;EAEF;IACE;;;AAGJ;EACE;IACE;IACA;IACA;IACA;IACA;IACA;IACA;IACQ;;;AAGZ;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;IACE;IACA;IACA;;;AAGJ;EACE;IACE;;EAEF;IACE;IACA;;EAEF;IACE;;;AAGJ;EACE;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;EACE;IACE;;EAEF;AAAA;IAEE;IACA;;EAEF;AAAA;AAAA;IAGE;IACA;;EAEF;AAAA;AAAA;IAGE;IACA;;;AAGJ;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;AAAA;EAIE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;AAAA;IAEE;IACA;;EAEF;AAAA;AAAA;IAGE;IACA;;EAEF;AAAA;AAAA;IAGE;IACA;;;AAGJ;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;AAAA;EAIE;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;IACE;IACA;;EAEF;AAAA;IAEE;IACA;;EAEF;AAAA;IAEE;;;AAGJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACK;EACG;;;AAEV;AAAA;EAEE;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;IACE;;EAEF;IACE;;;AAGJ;EACE;IACE;;EAEF;IACE;;;AAGJ;EACE;IACE;;EAEF;IACE;;;AAGJ;EACE;EACA;EACA;EACA;EACA;EACA;EACQ;;;AAEV;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACQ;EACR;EACK;EACG;;;AAEV;AAAA;EAEE;EACA;EACA;EACA;EACQ;;;AAEV;AAAA;EAEE;EACK;EACG;;;AAEV;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;EAGE;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EASE;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACQ;;;AAEV;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;EAKE;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;AAAA;EAGE;;;AAEF;AAAA;AAAA;EAGE;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQE;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQE;;;AAEF;AAAA;AAAA;AAAA;EAIE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;EAEE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAYE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAYE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;EAKE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACQ;;;AAEV;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACK;EACG;EACR;EACI;EACC;EACG;;;AAEV;EACE;EACI;EACC;EACG;;;AAEV;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACQ;EACR;EACA;EACA;EACA;EACA;EACQ;;;AAEV;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;IACE;IACA;;EAEF;IACE;IACQ;;EAEV;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACQ;EACR;EACA;EACA;EACA;EACQ;EAER;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACK;EACG;;;AAEV;AAAA;EAEE;;;AAEF;EACE;IACE;IACK;IACG;IAER;IACQ;IACR;IACQ;;EAEV;AAAA;IAEE;IACA;IACQ;;EAEV;AAAA;IAEE;IACA;IACQ;;EAEV;AAAA;AAAA;IAGE;IACA;IACQ;;;AAGZ;AAAA;AAAA;EAGE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;AAAA;EAEE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;AAAA;AAAA;EAIE;EACA;EACA;EACA;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;EAEE;EACA;;;AAEF;AAAA;EAEE;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;AAAA;AAAA;AAAA;IAIE;IACA;IACA;IACA;;EAEF;AAAA;IAEE;;EAEF;AAAA;IAEE;;EAEF;IACE;IACA;IACA;;EAEF;IACE;;;AAGJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAgCE;EACA;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAgBE;;;AAEF;EACE;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;EACA;EACA;EACA;EACA;;;AAEF;EACE;;;AAEF;EACE;;;AAEF;EACE;;AAEF;AAAA;AAAA;AAAA;EAIE;;;AAEF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAYE;;;AAEF;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;AAAA;IAEE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;AAAA;IAEE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;AAAA;IAEE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;AAAA;IAEE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;IACE;;;AAGJ;EACE;;;AAEF;EACE;IACE;;EAEF;IACE;;EAEF;IACE;;EAEF;AAAA;IAEE;;;AAGJ;EACE;;;AAEF;EACE;IACE;;;AAGJ;EACE;;;AAEF;EACE;IACE;;;AAGJ;EACE;;;AAEF;EACE;IACE;;;AAGJ;EACE;IACE;;;;ACjmNJ;EACE;IACE;;;AAIJ;EACE;;;AAIA;AAAA;AAAA;AAAA;AAAA;AAAA;EAME;EACA;;;AAGJ;AAAA;ENVE,oBMYoB;ENXlB,iBMWkB;ENVlB,gBMUkB;ENTlB,eMSkB;ENRlB,YMQkB;;;AFftB;AHRA;AAEA;AAEA;AAEA;AAEA;AGGA;AGXA;EACE,aNUO;EDVP;EACA;;;AOIA;EACE;;;AAKF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGE;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAKF;EADF;IAEI;;;AAIA;EADF;IAEI;IACA;;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI;;;AAIA;EACE;EACA;;AAIJ;EACE;EACA;EACA;EACA;EPpDV,oBOsD8B;EPrD5B,iBOqD4B;EPpD5B,gBOoD4B;EPnD5B,eOmD4B;EPlD5B,YOkD4B;;AAEpB;EARF;IASI;;;AAGF;EACE;;AAOE;EADF;IAEI;IACA;;;AAMR;EACE;EACA;;AAEA;EACE;;AAKN;EACE;EACA;EACA;EACA;;AAEA;EANF;IAOI;IACA;IACA;IACA;IACA;IACA;;;AAOV;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EATF;IAUI;IACA;;EAEA;IACE;IACA;;;AAIJ;EACE;EACA;EACA;;AAKE;EACE;;AAGF;EACE;EACA;EACA;EACA;EP5IR,oBO8I4B;EP7I1B,iBO6I0B;EP5I1B,gBO4I0B;EP3I1B,eO2I0B;EP1I1B,YO0I0B;;AAEpB;EACE;;AAKF;EACE;EACA;EACA;;AAKE;EADF;IAEI;IACA;;;AAMR;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;;AAQZ;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;IACA;;;AAIA;EADF;IAEI;;;AAKF;EACE;EACA;EACA;EACA;;AAEA;EANF;IAOI;IACA;;;AAIJ;EACE;;AAMF;EAFF;IAGI;;;AAIA;EADF;IAEI;IACA;;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI;;;AAIA;EACE;EACA;;AAIJ;EACE;EACA;EACA;EP9PV,oBOgQ8B;EP/P5B,iBO+P4B;EP9P5B,gBO8P4B;EP7P5B,eO6P4B;EP5P5B,YO4P4B;;AAEpB;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EAhBF;IAiBI;IACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAEA;EAHF;IAII;;;AAOF;EADF;IAEI;IACA;;;AAGF;EACE;EACA;;AAMA;EADF;IAEI;IACA;;;AAMR;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AASd;EACE;EACA;EACA;EACA;;AAEA;EANF;IAOI;;;AAIA;EADF;IAEI;IACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EP9WR,oBOgX4B;EP/W1B,iBO+W0B;EP9W1B,gBO8W0B;EP7W1B,eO6W0B;EP5W1B,YO4W0B;;AAEpB;EACE;EACA;;AAGF;EAbF;IAcI;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAOV;EACE;;AAEA;EAHF;IAII;IACA;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;IACA;IACA;;;AAIJ;EACE;EACA;EACA;;AAEA;EALF;IAMI;IACA;;;AAGF;EACE;;APpZR;EOuZU;EACA;;APvZV;EOsZU;EACA;;APtZV;EOqZU;EACA;;APrZV;EOoZU;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EPxcR,oBO0c4B;EPzc1B,iBOyc0B;EPxc1B,gBOwc0B;EPvc1B,eOuc0B;EPtc1B,YOsc0B;;AAEpB;EACE;;AAMR;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EP1dJ,oBO4dwB;EP3dtB,iBO2dsB;EP1dtB,gBO0dsB;EPzdtB,eOydsB;EPxdtB,YOwdsB;;AAEpB;EACE;EACA;;AAKN;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAIJ;EACE;EACA;;AAGE;EACE;;AAKE;EACE;;AAEA;EACE;;AASN;EACE;;AAMJ;EACE;;;AH7gBR;AIdA;EACE;;AAEA;EACE;;AAEA;EACE;;AAEA;EACE;;AAKN;EAfF;IAgBI;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAIJ;EACE;EACA;;AAIJ;EACE;;AAGE;EACE;;AAIJ;EAEI;IACE;;EAGF;IACE;;EAEA;IACE;;;AAMR;EAzBF;IA0BI;;;AAIA;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;IACA;IACA;;;AAMJ;EACE;IACE;IACA;;;AAMR;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EAtBF;IAuBI;;;;ACjIN;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EATF;IAUI;IACA;IACA;;;AAGF;EAfF;IAgBI;;;AAGF;EAnBF;IAoBI;;;AAGF;EAvBF;IAwBI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;ET7BR,gBS8B2B;ET7B3B,cS6B2B;ET5B3B,eS4B2B;ET3B3B,mBS2B2B;ET1B3B,WS0B2B;;AAGrB;EACE;EACA;ETnCR,gBSoC2B;ETnC3B,cSmC2B;ETlC3B,eSkC2B;ETjC3B,mBSiC2B;EThC3B,WSgC2B;;AAEnB;EALF;IAMI;;;AAIJ;EACE;EACA;ET7CR,gBS8C2B;ET7C3B,cS6C2B;ET5C3B,eS4C2B;ET3C3B,mBS2C2B;ET1C3B,WS0C2B;;AAEnB;EALF;IAMI;;;AAKN;EACE;EACA;EACA;;AAGE;EACE;EACA;ET9DV,gBS+D6B;ET9D7B,cS8D6B;ET7D7B,eS6D6B;ET5D7B,mBS4D6B;ET3D7B,WS2D6B;;AAIvB;EACE;EACA;EACA;EACA;EACA;ETxER,gBS0E2B;ETzE3B,cSyE2B;ETxE3B,eSwE2B;ETvE3B,mBSuE2B;ETtE3B,WSsE2B;EACnB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAjBF;IAkBI;;;;ALxGZ;AMlBA;EACE;EACA;EACA;EACA;AACA;EAEA;EACA;EACA;EACA;EACA;;AAEA;EAbF;IAcI;;;AAGF;EAjBF;IAkBI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;;AAEA;EAZF;IAaI;IACA;IACA;IACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;;AAEA;EAXF;IAYI;;;AAGF;EAfF;IAgBI;IACA;IACA;;;AAIJ;EV/BA,yBUgC0B;EV/B1B,sBU+B0B;EV9B1B,oBU8B0B;EV7B1B,qBU6B0B;EV5B1B,iBU4B0B;EACxB;;AAEA;EAJF;IAKI;;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI;IACA;IACA;;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EATF;IAUI;;;;AAKN;EACE;;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EAHF;IAII;IACA;IACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EATF;IAUI;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAKN;EACE;EACA;;AAEA;EAJF;IAKI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;;AAMR;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EVzOF,oBU2OsB;EV1OpB,iBU0OoB;EVzOpB,gBUyOoB;EVxOpB,eUwOoB;EVvOpB,YUuOoB;;AAEpB;EACE;EACA;;;AAKN;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAEA;EACE;;AAEA;EAHF;IAII;IACA;IACA;;;;AV7NN;EWnEA;IACE;;EAEF;IACE;;;AXkEF;EWtEA;IACE;;EAEF;IACE;;;AXqEF;EWzEA;IACE;;EAEF;IACE;;;AXwEF;EW5EA;IACE;;EAEF;IACE;;;AAIJ;EACE;EACA;EACA;AAUA;AAsDA;AAsBA;AA8UA;;AAlaA;EALF;IAMI;;;AAGF;EATF;IAUI;;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EXrBF,oBWuBsB;EXtBpB,iBWsBoB;EXrBpB,gBWqBoB;EXpBpB,eWoBoB;EXnBpB,YWmBoB;;AAEpB;EACE;EACA;;AAIA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EXfN,gBWiByB;EXhBzB,cWgByB;EXfzB,eWeyB;EXdzB,mBWcyB;EXbzB,WWayB;;AAIvB;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAMN;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAMN;EACE;EACA;EACA;EACA;EACA;AAEA;;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EAOE;;AALE;EADF;IX1EN,mBW4E6B;IX3E7B,gBW2E6B;IX1E7B,cW0E6B;IXzE7B,eWyE6B;IXxE7B,WWwE6B;;;AAOzB;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EATF;IAUI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAKF;EADF;IAEI;;;AAGF;EACE;EACA;;AAEA;EACE;;AAIJ;EACE;EACA,aVvLH;EUwLG;EACA;EACA;EACA;;AAEA;EARF;IASI;;;AAIJ;EACE;EACA,aVpMH;EUqMG;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EX5JR,yBW8JgC;EX7JhC,sBW6JgC;EX5JhC,oBW4JgC;EX3JhC,qBW2JgC;EX1JhC,iBW0JgC;;AAExB;EAdF;IAeI;;;AAGF;EAlBF;IAmBI;;;AAIJ;EACE;EACA;;AAEA;EAJF;IAKI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACG;EACE;EACG;;AAER;EACE;EACA;;AAEA;EAJF;IAKI;;;AAQR;EADF;IAEI;;;AAGF;EACE;EACA;;AAEA;EACE;;AAKF;EACE;EACA,aVxQL;EUyQK;EACA;EACA;EACA;EACA;;AAEA;EATF;IAUI;IACA;;;AAIJ;EACE;EACA,aVvRL;EUwRK;EACA;EACA;EACA;;AAGF;EX9OR,yBW+OkC;EX9OlC,sBW8OkC;EX7OlC,oBW6OkC;EX5OlC,qBW4OkC;EX3OlC,iBW2OkC;EACxB;;AAEA;EAJF;IAKI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EXjUd,oBWmUkC;EXlUhC,iBWkUgC;EXjUhC,gBWiUgC;EXhUhC,eWgUgC;EX/ThC,YW+TgC;;AAGtB;EA/BF;IAgCI;IACA;IACA;;;AAON;EACE;EACA;EACA;EACA;EACG;EACE;EACG;;AAKd;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;;AAEA;EACE;;AAIJ;EACE;EACG;EACE;EACG;;AAGV;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAVF;IAWI;;;AAGF;EAdF;IAeI;;;AAIJ;EXjWN,yBWkWgC;EXjWhC,sBWiWgC;EXhWhC,oBWgWgC;EX/VhC,qBW+VgC;EX9VhC,iBW8VgC;EACxB;EACA;;AAEA;EACE;;AAEA;EAHF;IAII;IACA;IACA;;;AASZ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EX/aF,oBWibsB;EXhbpB,iBWgboB;EX/apB,gBW+aoB;EX9apB,eW8aoB;EX7apB,YW6aoB;;AAEpB;EACE;EACA;EACA;EACA;;AAEA;EX/ZJ,gBWgayB;EX/ZzB,cW+ZyB;EX9ZzB,eW8ZyB;EX7ZzB,mBW6ZyB;EX5ZzB,WW4ZyB;EACnB;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EXnbJ,gBWqbuB;EXpbvB,cWobuB;EXnbvB,eWmbuB;EXlbvB,mBWkbuB;EXjbvB,WWibuB;EX/cvB,oBWgdwB;EX/ctB,iBW+csB;EX9ctB,gBW8csB;EX7ctB,eW6csB;EX5ctB,YW4csB;;;AAK1B;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAKN;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;;AAEA;EAHF;IAII;IACA;;;AAKA;EACE;;AAGF;EACE;;AAIJ;EACE;EACA;EX5gBN,oBW8gB0B;EX7gBxB,iBW6gBwB;EX5gBxB,gBW4gBwB;EX3gBxB,eW2gBwB;EX1gBxB,YW0gBwB;;AAGtB;EACE;EACA;EACA;EXphBN,oBWshB0B;EXrhBxB,iBWqhBwB;EXphBxB,gBWohBwB;EXnhBxB,eWmhBwB;EXlhBxB,YWkhBwB;;;ACjiB5B;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;;AAIJ;EACE;EACA;;AAEA;AAME;;AALA;EACE;EZgBJ,gBYfuB;EZgBvB,cYhBuB;EZiBvB,eYjBuB;EZkBvB,mBYlBuB;EZmBvB,WYnBuB;;AAKnB;EZUJ,gBYTyB;EZUzB,cYVyB;EZWzB,eYXyB;EZYzB,mBYZyB;EZazB,WYbyB;;AAGrB;EZMJ,gBYLyB;EZMzB,cYNyB;EZOzB,eYPyB;EZQzB,mBYRyB;EZSzB,WYTyB;EACnB;;AAEA;EAJF;IZMJ,gBYD2B;IZE3B,cYF2B;IZG3B,eYH2B;IZI3B,mBYJ2B;IZK3B,WYL2B;;;AAM3B;AAME;;AALA;EACE;EZPJ,gBYQuB;EZPvB,cYOuB;EZNvB,eYMuB;EZLvB,mBYKuB;EZJvB,WYIuB;;AAKnB;EZbJ,gBYcyB;EZbzB,cYayB;EZZzB,eYYyB;EZXzB,mBYWyB;EZVzB,WYUyB;;AAGrB;EZjBJ,gBYkByB;EZjBzB,cYiByB;EZhBzB,eYgByB;EZfzB,mBYeyB;EZdzB,WYcyB;EACnB;;AAEA;EAJF;IZjBJ,gBYsB2B;IZrB3B,cYqB2B;IZpB3B,eYoB2B;IZnB3B,mBYmB2B;IZlB3B,WYkB2B;;;;AAO7B;EACE;EACA;EACA;EACA;EZjDA;EACE;EACA;EACA;EACA;;;AYkDJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EZ7DA;EACE;EACA;EACA;EACA;;AY6DF;EAXF;IAYI;IACA;;;AAIA;EACE;EACA;EACA;EACA;;AAEA;EANF;IAOI;IACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EZnGJ,oBYqGwB;EZpGtB,iBYoGsB;EZnGtB,gBYmGsB;EZlGtB,eYkGsB;EZjGtB,YYiGsB;;AAEpB;EAVF;IAWI;IACA;;;AAGF;EACE;;AAIJ;EACE;EACA;;AAEA;EAJF;IAKI;;;AAGF;EACE;EACA;EACA;;AAEE;EACE;;AAIJ;EACE;EACA;EZrIR,oBYuI4B;EZtI1B,iBYsI0B;EZrI1B,gBYqI0B;EZpI1B,eYoI0B;EZnI1B,YYmI0B;;AAO1B;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EZ9JJ,oBYgKwB;EZ/JtB,iBY+JsB;EZ9JtB,gBY8JsB;EZ7JtB,eY6JsB;EZ5JtB,YY4JsB;;AAEpB;EAZF;IAaI;;;AAGF;EACE;;AAGF;EACE;EACA;EACA;;;AAMR;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;IACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EZvJF,yBYwJ0B;EZvJ1B,sBYuJ0B;EZtJ1B,oBYsJ0B;EZrJ1B,qBYqJ0B;EZpJ1B,iBYoJ0B;EACxB;;AAEA;EAZF;IAaI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EZrMF,yBYuM0B;EZtM1B,sBYsM0B;EZrM1B,oBYqM0B;EZpM1B,qBYoM0B;EZnM1B,iBYmM0B;;AAExB;EAVF;IAWI;;;AAGF;EAdF;IAeI;;;AAIJ;EACE;EACA;EZpNF,yBYsN0B;EZrN1B,sBYqN0B;EZpN1B,oBYoN0B;EZnN1B,qBYmN0B;EZlN1B,iBYkN0B;;AAExB;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;EACA;EZnRN,oBYoR0B;EZnRxB,iBYmRwB;EZlRxB,gBYkRwB;EZjRxB,eYiRwB;EZhRxB,YYgRwB;;AAEpB;EANF;IAOI;;;AAGF;EAVF;IAWI;IACA;IACA;;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAMR;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAKN;EACE;EACA;EACA;EACA;;AAEA;EACE;IACE;;;AAIJ;EACE;EACA;EACA;;AAGF;EACE;;AAEA;EAHF;IAII;IACA;;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;;;AAMR;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;IACI;;;AAIN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EAJF;IAKI;;;AAGF;EARF;IASI;;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAKF;EADF;IAEI;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;IACA;;;;AAON;EACE;EACA;;AAEF;EACE;;;AC7cJ;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;;AAEA;EAJF;IAKI;;;AAGF;EACE;EACA;;AAGE;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAKN;EACE;EACA;EACA;EACA;EACA;EbnDR,oBaqD4B;EbpD1B,iBaoD0B;EbnD1B,gBamD0B;EblD1B,eakD0B;EbjD1B,YaiD0B;;AAEpB;EACE;EACA;;AAMR;EACE;;AAEA;EACE;;AAEA;EAHF;IAII;IACA;IACA;IACA;IACA;IACA;;;AAIA;EADF;IAEI;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;IACA;;;AAMJ;EACE;EACA;;;AAQZ;EACE;;AAEA;EAHF;IAII;;;AAIA;EADF;IAEI;;;AAKF;EADF;IAEI;IACA;;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAKN;EACE;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;Eb9LJ,oBagMwB;Eb/LtB,iBa+LsB;Eb9LtB,gBa8LsB;Eb7LtB,ea6LsB;Eb5LtB,Ya4LsB;;AAEpB;EAbF;IAcI;;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACrPR;EACE;;AAEA;EACE;IACE;;;AAMA;EADF;IAEI;;;AAMJ;EADF;IAEI;;;AAGF;EALF;IAMI;;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;;AAMR;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EATF;IAUI;;;AAGF;EAEE;;AACA;EACE;;AAIJ;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAKN;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;;AAGF;EACE;;AAGF;EACE;;;AAMR;EACE;;AAEA;EACE;;AAEA;EACE;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI;IACA;IACA;;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;;AAOV;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;IACA;IACA;;;AAKN;EACE;;AAGE;EADF;IAEI;IACA;;;AAGF;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAKF;EADF;IAEI;IACA;IACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAOV;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;IACA;;;AAIJ;EACE;;AAEA;EAHF;IAII;IACA;;;;ACnTN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGE;EACE;EACA;;AAEA;EACE;EACA;EACA;EfNN,oBeQ0B;EfPxB,iBeOwB;EfNxB,gBeMwB;EfLxB,eeKwB;EfJxB,YeIwB;;AAEpB;EACE;EACA;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EATF;IAUI;;;AAGF;EAbF;IAcI;;;AAGF;EACE;;AAGE;EACE;;AAKF;EACE;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;;AAIJ;EACE;EACA;;AAGE;EACE;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAQZ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;;;ACnKN;EACE;EACA;EAEA;EACA;EACA;EACA;EACA;;AAGE;EACE;EACA;;AAEA;EACE;EACA;EACA;EhBPN,oBgBS0B;EhBRxB,iBgBQwB;EhBPxB,gBgBOwB;EhBNxB,egBMwB;EhBLxB,YgBKwB;;AAEpB;EACE;EACA;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;EACE;EACA;EACF;;AAGF;EACE;EhBhCF;EACE;EACA;EACA;EACA;EgBgCA;EACA;EACE;EACA;EACA;;AAEF;EhB1BF,gBgB2BuB;EhB1BvB,cgB0BuB;EhBzBvB,egByBuB;EhBxBvB,mBgBwBuB;EhBvBvB,WgBuBuB;;AAInB;EACE;;AAEF;EACE;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EAUA;EACA;EACA;EAEA;EACE;EACA;AAEA;AACA;AACA;EAEF;EACA;EACE;EACA;EACA;;AAxBF;EATF;IAUI;;;AAGF;EAbF;IAcI;;;AAqBA;EACE;;AAGJ;EACE;EhBhFJ,gBgBkFuB;EhBjFvB,cgBiFuB;EhBhFvB,egBgFuB;EhB/EvB,mBgB+EuB;EhB9EvB,WgB8EuB;;AAEnB;EACE;EACA;EACA;EACA;;AAIJ;EACE;;AAGE;EACE;;AAKF;EACE;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;;AAMR;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;;ACtLN;EACE;;AAEA;EACE;;AAEA;EACE;;AAGE;EACE;EACA;;AAIJ;EACE;EACA;EjBPN,oBiBQ0B;EjBPxB,iBiBOwB;EjBNxB,gBiBMwB;EjBLxB,eiBKwB;EjBJxB,YiBIwB;;AAEpB;EALF;IAMI;;;AAKN;AAKE;;AAJA;EADF;IAEI;;;AAIF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAKN;EACE,ahBnCG;EgBoCH;EACA;EACA;;AAEA;EANF;IAOI;IACA;;;AAGF;EAXF;IAYI;;;AAGF;EACE;EjBrDN,oBiBuD0B;EjBtDxB,iBiBsDwB;EjBrDxB,gBiBqDwB;EjBpDxB,eiBoDwB;EjBnDxB,YiBmDwB;;AAEpB;EACE;EACA;;AAKN;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE,ahB5EO;EgB6EP;EACA;EACA;EACA;;AAEA;EAPF;IAQI;;;AAIJ;EACE,ahBxFO;EgByFP;EACA;EACA;EjB/FJ,oBiBgGwB;EjB/FtB,iBiB+FsB;EjB9FtB,gBiB8FsB;EjB7FtB,eiB6FsB;EjB5FtB,YiB4FsB;;AAKtB;EADF;IAEI;;;AAKF;EADF;IAEI;IACA;;;AAGF;EACE;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EjB7HN,oBiB+H0B;EjB9HxB,iBiB8HwB;EjB7HxB,gBiB6HwB;EjB5HxB,eiB4HwB;EjB3HxB,YiB2HwB;;AAEpB;EACE;;AAIJ;EACE;EACA;EACA;EACA;EjB1IN,oBiB2I0B;EjB1IxB,iBiB0IwB;EjBzIxB,gBiByIwB;EjBxIxB,eiBwIwB;EjBvIxB,YiBuIwB;EACpB;EACA;;AAEA;EATF;IAUI;;;AAGF;EACE;;AjBlHR;EiBsHU;EACF;EACA;;AjBvHR;EiBqHU;EACF;EACA;;AjBtHR;EiBoHU;EACF;EACA;;AjBrHR;EiBmHU;EACF;EACA;;AAKN;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;;AAIJ;EACE;EACA;;AAEA;EAJF;IAKI;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EALF;IAMI;IACA;;;;ACrOV;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;;AAIJ;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;;AAEA;EAEI;IACE;;;AAKN;EAEI;IACE;;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;ElB9CJ,oBkBgDwB;ElB/CtB,iBkB+CsB;ElB9CtB,gBkB8CsB;ElB7CtB,ekB6CsB;ElB5CtB,YkB4CsB;;AAEpB;EAdF;IAeI;;;AAGF;EAlBF;IAmBI;IACA;;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;;;AAIJ;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;;AAKN;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;ElBhHF,oBkBkHsB;ElBjHpB,iBkBiHoB;ElBhHpB,gBkBgHoB;ElB/GpB,ekB+GoB;ElB9GpB,YkB8GoB;;AAEpB;EACE;EACA;EACA;;;AClIN;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAIJ;EACE;EACA;EACA;;;AAIJ;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EATF;IAUI;IACA;;;AAGF;EAdF;IAeI;;;AAIJ;EACE;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;AAEA;EAJF;IAKI;IACA;;;AAIJ;EACE;EACA;;AAEA;EAJF;IAKI;;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAVF;IAWI;;;AAGF;EAdF;IAeI;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACE;EACA;;AAGJ;EACE;;AAEA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;;;AAKF;EADF;IAEI;;;AAMR;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAIJ;EACE;;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EnB5MV,oBmB8M8B;EnB7M5B,iBmB6M4B;EnB5M5B,gBmB4M4B;EnB3M5B,emB2M4B;EnB1M5B,YmB0M4B;;AAEpB;EACE;;AAKN;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EnBjOR,oBmBmO4B;EnBlO1B,iBmBkO0B;EnBjO1B,gBmBiO0B;EnBhO1B,emBgO0B;EnB/N1B,YmB+N0B;;AAGlB;EACE;;AAIJ;EACE;EACA;EACA;EACA;EnB/OV,oBmBiP8B;EnBhP5B,iBmBgP4B;EnB/O5B,gBmB+O4B;EnB9O5B,emB8O4B;EnB7O5B,YmB6O4B;;AAK1B;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAIJ;EACE;EACA;;AAMR;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EnBxSJ,oBmB0SwB;EnBzStB,iBmBySsB;EnBxStB,gBmBwSsB;EnBvStB,emBuSsB;EnBtStB,YmBsSsB;;AAEpB;EACE;;;ACxTR;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA,anBfI;EmBgBJ;EACA;EACA;;AAEA;EATF;IAUI;IACA;IACA;IACA;;;AAGF;EAhBF;IAiBI;IACA;IACA;;;;AAKN;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAGF;EACE;;;AAIJ;EACE;EACA;;AAEA;EAJF;IAKI;;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAEA;EACE;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;;AAKN;EACE;;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EANF;IAOI;IACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAGF;EACE;;;AC1LN;EACE;;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;IACE;IACA;;EAGF;IACE;;;AAIJ;EAxBF;IAyBI;IACA;IACA;IACA;IACA;IACA;;EAEA;IACE;;;AAIJ;EArCF;IAsCI;;;AAGF;EACE;;AAEA;EAHF;IAII;IACA;IACA;;EAEA;IACE;;;AAIJ;EACE;EACA;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAKN;EACE;EACA;;AAMJ;EACE;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAEA;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAMR;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;IACA;IACA;;;;AAOV;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;IACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;;AAEA;EAdF;IAeI;IACA;IACA;IACA;IACA;IACA;;EAEA;IACE;;;AAIJ;EA3BF;IA4BI;;;AAGF;EACE;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAKN;EACE;EACA;;;AAMR;EACE;EACA;;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;IACE;;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAZF;IAaI;IACA;IACA;IACA;IACA;IACA;IACA;IACA;;EAEA;IACE;;;AAIJ;EA3BF;IA4BI;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;IACE;;EAGF;IACE;;;AAIJ;EAxBF;IAyBI;IACA;IACA;IACA;IACA;IACA;;EAEA;IACE;;;AAIJ;EArCF;IAsCI;;;AAGF;EACE;;AAEA;EAHF;IAII;IACA;IACA;;EACA;IACE;;;AAIJ;EACE;EACA;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAKN;EACE;EACA;;;AC/mBR;EACE;;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EtBvBJ,oBsByBwB;EtBxBtB,iBsBwBsB;EtBvBtB,gBsBuBsB;EtBtBtB,esBsBsB;EtBrBtB,YsBqBsB;;AAEpB;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAKN;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAOV;EACE;EACA;;AAEA;EACE;EACA;EtB5FJ;EACA;EACA;EACA;EACA;;AsB6FE;EACE;EACA;EACA;EACA;;;AAMJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAXF;IAYI;;;AAGF;EAfF;IAgBI;;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EANF;IAOI;;;AAMJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;;;AAIJ;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAKN;EACE;;AAGE;EACE;;AAIJ;EACE;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;EtBhPN;EACA;EACA;EACA;EACA;;AsBgPM;EARF;IASI;IACA;;;AAON;EACE;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAEA;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;;AC/TV;EACE;;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;;;AAKN;EACE;EACA;EACA;;AAGE;EACE;;AAIJ;EACE;;AAEA;EACE;;AAEA;EACE;EACA;EvBlCR,oBuBoC4B;EvBnC1B,iBuBmC0B;EvBlC1B,gBuBkC0B;EvBjC1B,euBiC0B;EvBhC1B,YuBgC0B;;AAEpB;EACE;;;AAQZ;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAIJ;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI;IACA;;;AAKN;EACE;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EvBjHJ,oBuBmHwB;EvBlHtB,iBuBkHsB;EvBjHtB,gBuBiHsB;EvBhHtB,euBgHsB;EvB/GtB,YuB+GsB;;AAEpB;EAVF;IAWI;;;AAGF;EACE;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;;ACnJN;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAXF;IAYI;;;AAGF;EACE;EACA;EACA;;AAGF;EACE;;;AAKN;EACE;;AAGE;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;AAyBA;;AAvBA;EAJF;IAKI;;;AAGF;AAAA;EAEE;;AAEA;EAJF;AAAA;IAKI;;;AAIJ;EACE;EACA;EACA;;AAGF;EACE;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAON;EADF;IAEI;;;AAGF;EACE;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;ExBrJN,oBwBuJ0B;ExBtJxB,iBwBsJwB;ExBrJxB,gBwBqJwB;ExBpJxB,ewBoJwB;ExBnJxB,YwBmJwB;;AAEpB;EACE;EACA;;AAGF;EACE;EACA;;;AC3KV;EACE;EACA;;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EALF;IAMI;IACA;IACA;IACA;;;AAIJ;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAKN;EACE;EACA;;AAIJ;EACE;EACA;;AAEA;EACE;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAMR;EACE;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAMR;EACE;;;AC/JJ;EACE;;AAEA;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;;;AAKN;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAGF;EACE;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;E1B3CR,oB0B6C4B;E1B5C1B,iB0B4C0B;E1B3C1B,gB0B2C0B;E1B1C1B,e0B0C0B;E1BzC1B,Y0ByC0B;;AAEpB;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;EACA;EACA;;AAOV;EACE;;AAGF;EACE;EACA;EACA;E1BjFF,oB0BmFsB;E1BlFpB,iB0BkFoB;E1BjFpB,gB0BiFoB;E1BhFpB,e0BgFoB;E1B/EpB,Y0B+EoB;;AAGlB;EACE;;AAEA;EACE;EACA;;AAKN;EACE;;AAEA;EACE;;AAEA;EACE;EACA;EACA;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;E1BrHJ,oB0BuHwB;E1BtHtB,iB0BsHsB;E1BrHtB,gB0BqHsB;E1BpHtB,e0BoHsB;E1BnHtB,Y0BmHsB;;AAEpB;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAKN;EACE;;;AAKN;AACA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAIJ;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;EACA;;;ACrOF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EANF;IAOI;;;AAKN;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;;AAEA;EACE;;AAIJ;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;EACA;;;AC7GN;EACE;EACA;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA,a3BGG;E2BFH;EACA;EACA;EACA;EACA;EACA;;AAKF;EACE;EACA;EACA;EACA;EACA;;AAIA;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAKF;EACE;;AAGE;EADF;IAEI;IACA;;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAGE;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;;;AC5GZ;EACE;EACA;EACA;EACA;;AAEA;EANF;IAOI;IACA;;;AAGF;EACE;;AAIA;EADF;IAEI;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EAPF;IAQI;IACA;;;AAKN;EACE;EACA;;AAEA;EAJF;IAKI;;;AAIJ;EACE;;AAGE;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;;A7BtBJ;E6ByBM;EACA;EACA;;A7B1BN;E6BwBM;EACA;EACA;;A7BzBN;E6BuBM;EACA;EACA;;A7BxBN;E6BsBM;EACA;EACA;;AAKF;EACE;;;AAMR;EACE;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAEA;EAHF;IAII;;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;E7BjGJ,oB6BkGwB;E7BjGtB,iB6BiGsB;E7BhGtB,gB6BgGsB;E7B/FtB,e6B+FsB;E7B9FtB,Y6B8FsB;;AAEpB;EACE;EACA;EACA;;AAGF;EACE;;AAIJ;EACE;EACA;;;AC5HN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;AAEA;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAMJ;EACE;EACA;EACA;EACA;EACA;;;AAKN;EACE;;AAEA;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAKN;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;;ACzHJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGE;EACE;;AAGF;EACE;EACA;;AAOE;EACE;;AAON;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;E/BvCN,oB+ByC0B;E/BxCxB,iB+BwCwB;E/BvCxB,gB+BuCwB;E/BtCxB,e+BsCwB;E/BrCxB,Y+BqCwB;;AAEpB;EACE;EACA;EACA;;AAOF;EACE;;AAMR;EACE;E/B7DF,oB+B8DsB;E/B7DpB,iB+B6DoB;E/B5DpB,gB+B4DoB;E/B3DpB,e+B2DoB;E/B1DpB,Y+B0DoB;EACpB;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;E/BhFJ,oB+BiFwB;E/BhFtB,iB+BgFsB;E/B/EtB,gB+B+EsB;E/B9EtB,e+B8EsB;E/B7EtB,Y+B6EsB;EACpB;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;AAIA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;E/BzGN,oB+B0G0B;E/BzGxB,iB+ByGwB;E/BxGxB,gB+BwGwB;E/BvGxB,e+BuGwB;E/BtGxB,Y+BsGwB;;AAGtB;EACE;EACA;EACA;EACA;;AAKN;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA,a9B3HG;E8B4HH;;AAEA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAIJ;EACE;;AAEA;EACE;EACA;;AAEA;EAJF;IAKI;;;AAGF;EARF;IASI;;EAEA;IACE;;EAGF;IACE;IACA;;EAEA;IAEE;IACA;;EAKA;IACE;;EAIJ;IACE;;;AASF;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAMR;EACE;EACA;;AAEA;EAJF;IAKI;;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI;;;AAIJ;E/BjNF;EACA;EACA;EACA;EACA;E+B+MI;EACA;EACA;EACA;;AAIJ;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;;AAMF;EACE;;AAIJ;EACE;EACA;;;AC7QL;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;;AAEA;EACC;EACA,a/BNK;E+BOL;EACA;EACA;EACA;EhCbF,oBgCcsB;EhCbpB,iBgCaoB;EhCZpB,gBgCYoB;EhCXpB,egCWoB;EhCVpB,YgCUoB;;AAEpB;EACC;EACA;;AASA;EACC;;AAGD;EACC;;AAIF;EACC;EACA;EACA;EACA;EhCxCH,oBgC0CuB;EhCzCrB,iBgCyCqB;EhCxCrB,gBgCwCqB;EhCvCrB,egCuCqB;EhCtCrB,YgCsCqB;;AAEpB;EACC;;AAIF;EACC;EACA;;AAGC;EACC;EACA;EACA;;AASN;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;;AAEA;EACC;;AAKH;AAEC;AACC;AACC;AACC;AACA;AACD;AAEA;AACC;AACA;AACA;AACD;AACD;AACD;;AAGC;EACC;;AAIF;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;;AAGD;EACC;EACA;;AAEA;EACC;EACA;EACA;;AAGD;EACC;EACA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;;AAOL;EACC;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;;AAEA;EACC;EACA;EACA;EAUA;AAEA;AACC;AACA;AACD;;AAbA;EACC;;AAGD;EACC;;AAWF;AACC;AAEA;AACC;AACA;AACD;AAEA;AACC;AACD;AACA;AACC;AACD;AACA;AACC;AACD;AACA;AACC;AACD;AACA;AACC;AACD;;AAED;EACC;;AAEA;EACC;EACA;;AAGD;EACC;;AAED;EACC;;AAED;EACC;;AAED;EACC;;AAED;EACC;;AAGF;EACC;;AAEA;EACC;EACA;;AAGD;EACC;;AAED;EACC;;AAED;EACC;;;ACtST;EACE;EACA;;AAGE;EADF;IAEI;;;AAIJ;EACE;;AAEA;EACE;EACA;EACA;EjCLJ,oBiCOwB;EjCNtB,iBiCMsB;EjCLtB,gBiCKsB;EjCJtB,eiCIsB;EjCHtB,YiCGsB;;AAEpB;EACE;EACA;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;;AAGF;EACE;IACE;IACA;;;AAKN;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;IACE;IACA;;;AAKN;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAGF;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAMR;EACE;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;IACA;IACA;;;AAGF;EACE;EACA;EACA;;AAIJ;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAKN;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EjC5KR,oBiC8K4B;EjC7K1B,iBiC6K0B;EjC5K1B,gBiC4K0B;EjC3K1B,eiC2K0B;EjC1K1B,YiC0K0B;;AAEpB;EACE;EACA;EACA;;AAGF;EACE;EACA;;AAOV;EACE;EACA;EACA;EACA;EACA;;;AC/MJ;EACE;;;AAGF;EACE;EACA;AAwCA;;AAtCA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAKJ;EACE;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;;AAEA;EAJF;IAKI;IACA;;;AAIJ;EACE;EACA;;AAEA;EAJF;IAKI;;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EATF;IAUI;IACA;;;AAGF;EAdF;IAeI;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAVF;IAWI;;;AAGF;EAdF;IAeI;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACE;EACA;;AAGJ;EACE;;AAEA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAIJ;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAIJ;EACE;;AAEA;EAHF;IAII;;;AAMR;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAIJ;EACE;EACA;EACA;EACA;EAEA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;;A9BtMV;EACE;;AAGF;EACE;EACA;;;AAIJ;EACE;;;AAGF;EACE;EACA;EACA;;AAEA;EALF;IAMI;;;AAEF;EARF;IASI;;;;AAIJ;EACE;;;AAIA;EACE;;;AAIJ;EACE;IACE;IACA;IACA;;EAEA;IACE;IACA;;EAGF;IACE;IACA;IACA;;EAGF;IACE;;EAEA;IACE;;EAGF;IACE;;EAIJ;IACE;;EAGF;IACE;;;AAKN;EACE;IACE;IACA;;EAEA;IACE;IACA;;EAGF;IACE;;EAGF;IACE;;EAGF;IACE;IACA;IACA;;EAEA;IACE;;EAGF;IACE;;;AAQN;EACE;EACA;;;AAIJ;EACE;EACA;AACA;EACA;AACA;EACA;EACA;AACA;;AAEA;EACE;;AAGF;EACE;EACA;EACA;;AAEA;EACE;;AAKF;EACE;;AAEA;EACE;;AAKN;EACE;EACA;EACA;EACA;;;AAIJ;EACE;EACA;EACA;;;AAIA;EACE;EACA;;AAGF;EACE;;AAGF;EACE;;;AAIJ;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAIA;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;EACE;;;AAMJ;EACE;;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;AACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;AACE;;AAGF;EACE;;AAGF;EACE;;AAMJ;EACE;EACA;EACA;EACA;EACA;AAIA;AACE;AACA;AACF;;AAEA;EAOE;AACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAbA;EADF;IAEI;;;AAEF;EAJF;IAKI;;;AAWF;EACE;EACA;EACA;;;AAOV;EACE;EACA;;;AAGF;EAEE;;;AAGF;EACE;;;AAIF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAKE;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAMR;EACE;EACA;;AAEA;EACE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;AACA;EAEA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AAGF;EACE;EACA;;;AAIJ;EACE;;;AAKA;EACE;;AAGF;EACE;;;AAKF;EACE;EACA;;;AAIJ;EACE;EACA;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAIJ;EACE;EACA;AAEA;;;AAGF;EACE;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;EACA;;AAEA;EACE;;;AAIJ;EACE;AACA;EACA;EACA;EACA;EACA;;;AAIA;EACE;;;AAIJ;EACE;EACA;;;AAIA;EACE;EACA;EACA;;;AAIJ;EACE;;;AAMI;EACE;;AAEF;EACE;;AAEF;EACE;;AAEF;EACE;EACA;;AAEF;EACE;;AAEF;EACE;;AAEF;EACE;;AAEF;EACE;EACA;;AAKF;EACE;;AAKF;EACE;;AAGF;EACE;;AAEF;EACE;;AAEF;EACE;;AAEF;EACE;;AAEF;EACE;;AAEF;EACE;;;AAMR;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAIF;AACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;AACA;EACA;;;AAKN;EACE;;;AAGF;EAME;EACA;;AAGE;EACE;EACA;;AAKF;EACE;;AAGF;EACE,OApBM;;AAuBR;EACE,OAtBG;EAuBH;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE,OApCK;EAqCL;;AAGF;EACE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;;AAIJ;EACE,OA1DQ;;AA8DR;EACE,OA/DM;EAgEN;;AAGF;EACE,OApEM;;AAuER;EACE,OAxEM;;AA2ER;EACE,OAzEG;EA0EH;;AAGF;EACE;;AAGF;EACE,OAnFK;;AAsFP;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA,OApGK;;AAuGP;EACE;;AAGF;EACE;EACA;;AAGF;EACE,OAjHK;;AAqHT;EACE;EACA;;AAGF;EACE;;AAIA;EACE;;;AAKN;EAGE;EACA;EACA;EACA,WALO;EAMP;EACA;EACA;;AAEA;EAXF;IAYI;;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAIF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAIA;EACE;;AAEA;EAHF;IAII;;;AAKF;EACE;;AAEA;EAHF;IAII;;;AAMJ;EACE;;AAKN;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;;AAGF;EACE;EACA;;AAEA;EAJF;IAKI;;;AAIA;EADF;IAEI;;;AAKN;EACE;;AAEA;EACE;;;AAMJ;EACE;;AAGE;EACE","sourcesContent":["@mixin font-smoothing {\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n@mixin font-smoothing-none {\n -webkit-font-smoothing: auto;\n -moz-osx-font-smoothing: auto;\n}\n\n@mixin transition($transitions) {\n -webkit-transition: $transitions;\n -moz-transition: $transitions;\n -ms-transition: $transitions;\n -o-transition: $transitions;\n transition: $transitions;\n}\n\n/* this mixin makes possible to add transition to a specific vendor, example: */\n/* -webkit-transition: -webkit-transform 0.5s linear; */\n@mixin transitionPrefix($property, $values) {\n -webkit-transition: -webkit-#{$property} #{$values};\n -moz-transition: -moz-#{$property} #{$values};\n -ms-transition: -ms-#{$property} #{$values};\n -o-transition: -o-#{$property} #{$values};\n transition: #{$property} #{$values}; \n}\n\n@mixin background-gradient($from, $to) {\n background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, $from), color-stop(100%, $to));\n background: -webkit-linear-gradient($from, $to);\n background: -moz-linear-gradient($from, $to);\n background: -o-linear-gradient($from, $to);\n background: linear-gradient($from, $to);\n}\n\n@mixin transform ($transforms) {\n -moz-transform: $transforms;\n -o-transform: $transforms;\n -ms-transform: $transforms;\n -webkit-transform: $transforms;\n transform: $transforms;\n}\n\n@mixin placeholder {\n &::-webkit-input-placeholder {@content}\n &:-moz-placeholder {@content}\n &::-moz-placeholder {@content}\n &:-ms-input-placeholder {@content} \n}\n\n@mixin animation($animations) {\n -webkit-animation: $animations;\n -moz-animation: $animations;\n -o-animation: $animations;\n -ms-animation: $animations;\n animation: $animations;\n}\n\n@mixin animationDelay($value) {\n -webkit-animation-delay: $value;\n -moz-animation-delay: $value;\n -o-animation-delay: $value;\n -ms-animation-delay: $value;\n animation-delay: $value;\n}\n\n@mixin keyFrame($name) {\n @-webkit-keyframes $name{\n @content\n }\n @-moz-keyframes $name{\n @content\n }\n @-o-keyframes $name{\n @content\n }\n @keyframes $name{\n @content\n }\n}\n","/* roboto font */\n@import url(https://fonts.googleapis.com/css?family=Roboto:400,100,100italic,300italic,300,400italic,500,500italic,700,700italic,900,900italic);\n/* source sans pro */\n@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,600,700,900,200italic,300italic,400italic,600italic,700italic,900italic);\n/* lato */\n@import url(https://fonts.googleapis.com/css?family=Lato:100,300,400,700,900,100italic,300italic,400italic,700italic,900italic);\n/* merriweather */\n@import url(https://fonts.googleapis.com/css?family=Merriweather:400,300italic,300,400italic,700,700italic);\n/* karla */\n@import url(https://fonts.googleapis.com/css?family=Karla:400,700);\n\n$roboto: \"Roboto\";\n$lato: 'Lato', 'Helvetica Neue', Arial;\n$helvetica: \"Helvetica Neue\", Helvetica, Arial;\n$myriad: Myriad Pro, Lato, Helvetica Neue, Arial;\n$sourceSans: Source Sans Pro, Helvetica Neue, Arial;\n$merriweather: \"Merriweather\", Georgia;\n$karla: 'Karla', 'Lato', 'Helvetica Neue', Arial;","*:focus {\n outline: 0;\n}\n\n.button {\n display: inline-block;\n vertical-align: middle;\n zoom: 1;\n padding: 13px 32px;\n font-weight: 400;\n font-size: 17px;\n color: #fff !important;\n text-shadow: rgba(0,0,0,0.2) 0 -1px 0;\n border: 0px;\n border-radius: 5px;\n box-shadow: rgba(0, 0, 0, 0.3) 0 1px 2px, inset rgba(255, 255, 255, 0.88) 0px 1px 3px -1px;\n background-color: #4589E3;\n\n @include background-gradient(#5DA4FF, #417BFF);\n @include transition(all 0.2s linear);\n\n &:hover {\n color: #fff;\n text-decoration: none;\n }\n\n &:active {\n box-shadow: inset 0px 3px 3px rgba(0, 0, 0, 0.29);\n }\n\n &.button-small {\n padding: 10px 33px;\n border: 1px solid #68A2EE;\n font-size: 14px;\n font-weight: 500;\n }\n}\n\n.button-outline {\n display: inline-block;\n vertical-align: middle;\n zoom: 1;\n color: #fff;\n padding: 12px 31px;\n border: 2px solid #fff;\n border-radius: 4px;\n font-size: 16px;\n font-weight: 400;\n background: rgba(0, 0, 0, 0.15);\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);\n\n @include transition(all 0.2s linear);\n\n &:hover {\n text-decoration: none;\n color: #ddd;\n border-color: #ddd;\n }\n}\n\n.button-clear {\n border: 0;\n outline: none;\n padding: 1px;\n display: inline-block;\n text-decoration: none;\n -webkit-border-radius: 5px;\n -moz-border-radius: 5px;\n -ms-border-radius: 5px;\n -o-border-radius: 5px;\n border-radius: 5px;\n -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075);\n -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075);\n -ms-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075);\n -o-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075);\n\n background: #adb2bb;\n background: -webkit-linear-gradient(#ccd0d6, #adb2bb);\n background: -moz-linear-gradient(#ccd0d6, #adb2bb);\n background: -ms-linear-gradient(#ccd0d6, #adb2bb);\n background: -o-linear-gradient(#ccd0d6, #adb2bb);\n background: linear-gradient(#ccd0d6, #adb2bb); \n\n span {\n display: block;\n font-size: 14px;\n padding-left: 23px;\n padding-right: 23px;\n height: 36px;\n line-height: 33px;\n border-radius: 4px;\n font-weight: bold;\n\n color: #444;\n text-shadow: 0 1px 0 white;\n background: #eff1f4;\n background: -webkit-linear-gradient(#f7f8fa, #eff1f4);\n background: -moz-linear-gradient(#f7f8fa, #eff1f4);\n background: -ms-linear-gradient(#f7f8fa, #eff1f4);\n background: -o-linear-gradient(#f7f8fa, #eff1f4);\n background: linear-gradient(#f7f8fa, #eff1f4);\n -webkit-box-shadow: inset 0 1px 0 white;\n -moz-box-shadow: inset 0 1px 0 white;\n -ms-box-shadow: inset 0 1px 0 white;\n -o-box-shadow: inset 0 1px 0 white;\n box-shadow: inset 0 1px 0 white;\n }\n\n &:hover {\n text-decoration: none;\n }\n\n &:active,\n &.active {\n background: #b5bac2;\n background: -webkit-linear-gradient(#b5bac2, #b5bac2);\n background: -moz-linear-gradient(#b5bac2, #b5bac2);\n background: -ms-linear-gradient(#b5bac2, #b5bac2);\n background: -o-linear-gradient(#b5bac2, #b5bac2);\n background: linear-gradient(#b5bac2, #b5bac2);\n\n span {\n color: #444;\n background: #eff1f4;\n background: -webkit-linear-gradient(#e6e8eb, #eff1f4);\n background: -moz-linear-gradient(#e6e8eb, #eff1f4);\n background: -ms-linear-gradient(#e6e8eb, #eff1f4);\n background: -o-linear-gradient(#e6e8eb, #eff1f4);\n background: linear-gradient(#e6e8eb, #eff1f4);\n -webkit-box-shadow: inset 0 1px 0 rgba(0,0,0,0.1);\n -moz-box-shadow: inset 0 1px 0 rgba(0,0,0,0.1);\n -ms-box-shadow: inset 0 1px 0 rgba(0,0,0,0.1);\n -o-box-shadow: inset 0 1px 0 rgba(0,0,0,0.1);\n box-shadow: inset 0 1px 0 rgba(0,0,0,0.1);\n }\n }\n}\n\n\n/* form controls */\n\ninput[type=\"text\"],\ninput[type=\"email\"],\ninput[type=\"password\"] {\n\n &.form-control {\n box-shadow: inset 0 1px 1px rgba(0,0,0,0.2);\n -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.2);\n -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,0.2);\n height: 39px;\n\n @include placeholder {\n color: #B1B1B1;\n font-weight: 400;\n font-size: 14px;\n }\n &:focus {\n border-color: #98D2FF;\n outline: 0;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px rgba(166, 205, 236, 0.6);\n }\n }\n}\n\ntextarea {\n &.form-control {\n box-shadow: inset 0 1px 1px rgba(0,0,0,0.2);\n -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.2);\n -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,0.2);\n\n @include placeholder {\n color: #B1B1B1;\n font-weight: 400;\n font-size: 14px;\n }\n\n &:focus {\n border-color: #98D2FF;\n outline: 0;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px rgba(166, 205, 236, 0.6);\n }\n }\n}\n\n\n",".btn-shadow {\n border: 0;\n font-weight: 600;\n color: #fff;\n display: inline-block;\n box-shadow: rgba(23, 43, 99, 0.24) 0 7px 28px;\n text-transform: uppercase;\n letter-spacing: 1px;\n font-size: 13px;\n padding: 12px 23px;\n text-shadow: 1px 1px rgba(0, 0, 0, 0.09);\n border-radius: 4px;\n\n @include transition(all .2s);\n\n &:hover,\n &:focus {\n color: #fff;\n text-decoration: none;\n }\n\n &:active {\n box-shadow: rgba(23, 43, 99, 0.24) 0 7px 28px, inset 0 2px 3px rgba(0, 0, 0, 0.3);\n }\n\n /* sizes */\n\n &.btn-shadow-sm {\n font-size: 11px;\n padding: 7px 14px;\n }\n\n &.btn-shadow-lg {\n font-size: 14px;\n padding: 16px 31px;\n }\n\n /* colors */\n\n &.btn-shadow-default {\n background: #fff;\n color: #444;\n text-shadow: none;\n }\n\n &.btn-shadow-primary {\n background: linear-gradient(#2083fe, #1d7bf0);\n }\n\n &.btn-shadow-dark {\n background: #2C394D;\n }\n\n &.btn-shadow-info {\n background: #42B1E2;\n }\n\n &.btn-shadow-warning {\n background: #E2B477;\n }\n\n &.btn-shadow-success {\n background: #0ad6bc;\n }\n}\n","@import \"modules/mixins\";\n@import \"modules/elements\";\n@import \"modules/buttons\";\n\n/* vendor */\n@import \"bootstrap/bootstrap\";\n@import \"bootstrap/overrides\";\n\n/* config variables */\n@import \"partials/config\";\n\n/* layout */\n@import \"partials/layout\";\n\n/* snippets */\n@import \"partials/snippets/features\";\n@import \"partials/snippets/slider\";\n\n/* pages */\n@import \"partials/pages/index\";\n@import \"partials/pages/index2\";\n@import \"partials/pages/index3\";\n@import \"partials/pages/features\";\n@import \"partials/pages/services\";\n@import \"partials/pages/signup\";\n@import \"partials/pages/signup_rotate\";\n@import \"partials/pages/blog\";\n@import \"partials/pages/blog_cols\";\n@import \"partials/pages/blog_timeline\";\n@import \"partials/pages/blogpost\";\n@import \"partials/pages/pricing\";\n@import \"partials/pages/charts\";\n@import \"partials/pages/aboutus\";\n@import \"partials/pages/contactus\";\n@import \"partials/pages/status\";\n@import \"partials/pages/gallery\";\n@import \"partials/pages/portfolio\";\n@import \"partials/pages/portfolio_item\";\n@import \"partials/pages/support\";\n@import \"partials/pages/404\";\n@import \"partials/pages/coming_soon\";\n@import \"partials/pages/docs\";\n@import \"partials/pages/invoice\";\n@import \"partials/pages/timeline\";\n\n#docs {\n #guide {\n margin-top: 70px;\n }\n\n #api-docs {\n margin-left: 200px !important;\n margin-top: -20px !important;\n }\n}\n\n.index-sidebar-hero .cta a.button {\n padding: 12px 30px;\n}\n\na.anchor {\n display: block;\n position: relative;\n visibility: hidden;\n\n @media (min-width: 1px) {\n top: -80px;\n }\n @media (min-width: 768px) {\n top: -140px;\n }\n}\n\n.table-small {\n font-size: 80%;\n}\n\na.header {\n &:hover {\n text-decoration: none !important;\n }\n}\n\n@media (min-width: 1px) {\n .navbar.normal {\n min-height: 75px;\n max-height: 75px;\n margin-bottom: 75px;\n\n .navbar-brand img {\n width: 130px;\n height: 130px;\n }\n\n .navbar-nav {\n font-size: 22px;\n position: relative;\n bottom: 15px;\n }\n\n .nav-links {\n left: 140px;\n\n .quickstart {\n display: none;\n }\n\n .tutorial {\n display: none;\n }\n }\n\n .nav li {\n display: inline-block;\n }\n\n .nav .li a {\n padding: 10px 5px\n }\n }\n}\n\n@media (min-width: 768px) {\n .navbar.normal {\n min-height: 125px;\n margin-bottom: 125px;\n\n .navbar-brand img {\n width: 200px;\n height: 200px;\n }\n\n .nav.gh {\n margin-top: 50px;\n }\n\n .nav .li a {\n padding: 10px 5px\n }\n\n .nav-links {\n font-size: 32px;\n left: 190px;\n top: 10px;\n\n .quickstart {\n display: inline-block;\n }\n\n .tutorial {\n display: inline-block;\n }\n }\n }\n}\n\n.guides-video {\n\n iframe {\n width: 100%;\n margin: 0 auto;\n }\n}\n\n.navbar.normal {\n position: fixed;\n width: 100%;\n /*background: url(\"https://www.toptal.com/designers/subtlepatterns/patterns/connectwork.png\");*/\n background: url(\"/assets/img/fancy-cushion.png\");\n /*background: url(\"https://www.toptal.com/designers/subtlepatterns/patterns/dark-honeycomb.png\");*/\n border-bottom: 1px solid black;\n box-shadow: 1px 1px 15px -2px black;\n /*background: black;*/\n\n .navbar-header {\n margin-top: -50px;\n }\n\n .navbar-nav {\n margin-top: 30px;\n font-family: 'Boogaloo', cursive;\n font-weight: bold;\n\n a {\n color: white !important;\n }\n }\n\n .navbar-brand {\n img {\n transition: all 200ms;\n\n &:hover {\n transform: rotate(-10deg);\n }\n }\n }\n\n .star {\n position: absolute;\n bottom: 0px;\n right: 65px;\n font-size: 26px;\n }\n}\n\nstrong {\n font-weight: bold;\n text-shadow: 2px 2px black;\n color: white;\n}\n\n.nav-menu .main-menu {\n h4 {\n color: darken(white, 10%);\n margin-left: 5rem;\n }\n\n ul {\n margin-top: 2rem;\n }\n\n ul li a {\n padding-left: 3rem;\n }\n}\n\n.spacer {\n width: 100%;\n margin-bottom: 25px;\n}\n\nimg.drop {\n box-shadow: 2px 2px 6px 1px rgba(0, 0, 0, 0.3);\n}\n\nblockquote {\n border: 1px solid black !important;\n}\n\n.layout-home {\n .brand {\n display: block;\n margin-left: auto;\n margin-right: auto;\n width: 300px;\n text-align: center;\n padding-top: 200px;\n\n .graphiti {\n font-size: 50px;\n }\n\n .desc {\n color: #b0b0b0;\n }\n }\n}\n\n.tutorial {\n ul {\n padding-left: 20px;\n }\n}\n\nbody {\n background: #414B5B;\n font-size: 17px;\n line-height: 1.7;\n color: lighten(#333, 10%);\n\n &.layout-home {\n background-image: url(\"/assets/img/home-bg.jpg\");\n background-attachment: fixed;\n background-position: center;\n background-repeat: no-repeat;\n /*background-size: cover;*/\n\n .holder {\n position: absolute;\n top: 20%;\n left: 50%;\n width: 500px;\n height: 500px;\n margin-top: -250px; /* Half the height */\n margin-left: -250px;\n transition: 200ms all;\n\n &:hover {\n /*margin-top: -260px;*/\n }\n\n a:hover {\n text-decoration: none;\n }\n\n h1 {\n margin-top: -75px;\n }\n }\n }\n\n &:not(.layout-home) {\n .page-content {\n background-image: url(\"https://images.unsplash.com/photo-1487452066049-a710f7296400?ixlib=rb-1.2.1&auto=format&fit=crop&w=2102&q=80\");\n background-attachment: fixed;\n background-position: center;\n background-repeat: no-repeat;\n background-size: cover;\n\n // so specific it overrides h1 h2\n // find diff way to display tweets\n /*:not(.twitter-tweet) {*/\n /*text-shadow: 1px 1px black;*/\n /*font-family: 'Noto Sans', sans-serif;*/\n /*}*/\n\n .wrapper > div.container {\n @media (min-width: 1px) {\n padding-top: 80px;\n }\n @media (min-width: 768px) {\n padding-top: 150px;\n }\n background: url(\"https://storage.googleapis.com/subtlepatterns-production/designers/subtlepatterns/uploads/dark-honeycomb.png\");\n /*background: #5c666f;*/\n border: 1px solid black;\n color: lighten(#ddd, 5%);\n font-size: 18px;\n border-radius: 5px;\n border: 1px solid black;\n box-shadow: -2px 2px 6px 2px rgba(0,0,0,0.4);\n\n img {\n border-radius: 5px;\n border: 1px solid black;\n box-shadow: 2px 2px 6px 1px rgba(0, 0, 0, 0.3);\n }\n }\n }\n }\n}\n\n#next {\n float: right;\n margin: 40px 0 40px 0;\n}\n\nh1 {\n //36\n font-size: 40px;\n}\n\nh2 {\n font-size: 36px;\n //\n}\n\nh1, h2 {\n font-family: 'Boogaloo', cursive;\n color: white;\n text-shadow: 4px 4px black;\n\n a, a:hover {\n color: black;\n text-decoration: none;\n position: relative;\n }\n\n a {\n &:hover {\n &:before {\n content: '\\00b6';\n position: absolute;\n left: -18px;\n color: grey;\n font-size: 18px;\n font-family: sans-serif;\n top: 4px;\n }\n }\n }\n}\n\nh3, h4, h5 {\n font-family: 'Boogaloo', cursive;\n color: white;\n\n a, a:hover {\n color: white;\n text-decoration: none;\n }\n}\n\nh5 {\n color: white;\n font-size: 17px;\n margin-top: 30px;\n border-bottom: 1px solid #f0f0f0;\n}\n\nh4 {\n color: white;\n margin-top: 40px;\n border-bottom: 1px solid darken(#f0f0f0, 10%);\n}\n\n.main-footer {\n background: url(\"https://www.toptal.com/designers/subtlepatterns/patterns/dark-honeycomb.png\");\n margin-top: 0;\n bottom: 0;\n}\n\n.get-started {\n margin-top: 40px;\n padding: 40px 20px;\n background: -webkit-linear-gradient(top, #355069 0, #5E7A9B 100%);\n /*background: radial-gradient(#36414E, #253241);*/\n\n font-size: 16px;\n color: #f8f8f8;\n font-weight: 400;\n line-height: 25px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n\n h2 {\n color: white;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n }\n\n a {\n color: #90CAF9;\n font-weight: bold;\n }\n}\n\n#docs #api-docs #methods .method .method-section {\n padding-top: 0;\n}\n\n// hack bc I can't get jekyll variables in sass atm\n#cover-image {\n &.prod-bg {\n background-image: url(\"/jsonapi_suite/assets/img/sunrise.png\") !important;\n }\n\n &.dev-bg {\n background-image: url(\"/assets/img/sunrise.png\") !important;\n }\n}\n\n.solution {\n img {\n margin-left: 30px !important;\n height: 300px;\n }\n}\n\ncode span {\n font-family: 'Roboto Mono', sans-serif !important;\n text-shadow: none !important;\n}\n\npre {\n background: #292E33;\n border: 1px solid black;\n\n code {\n font-family: 'Roboto Mono', sans-serif;\n text-shadow: none !important;\n font-size: 15px !important;\n }\n\n code.hljs {\n background: #292E33;\n color: lighten(#DAE4F2, 5%);\n line-height: 20px;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n}\n\ncode.highlighter-rouge {\n background: darken(#292E33, 5%);\n color: lighten(green, 60%);\n\n /*color: lighten(#c7254e, 10%);*/\n}\n\na, a:not([href]):not([tabindex]) {\n color: #90CAF9;\n cursor: pointer;\n font-weight: bold;\n text-shadow: 1px 1px black;\n\n &:hover {\n color: lighten(#90CAF9, 10%);\n }\n}\n\nimg[alt=github] {\n margin-right: 5px;\n width: 20px;\n height: 20px;\n}\n\n.toc {\n font-size: 15px;\n line-height: 1.8;\n\n > ul {\n padding-left: 10px;\n }\n}\n\n.js-toc, .ruby-toc, blockquote {\n background: #292E33;\n /*background: #f7f8fb;*/\n border: 1px solid black;\n border-radius: 5px;\n padding: 10px;\n line-height: 1.8;\n}\n\n.ruby-toc {\n ul {\n padding-left: 25px;\n }\n}\n\n.ruby-toc + div {\n margin-bottom: 20px;\n font-size: 16px;\n}\n\nblockquote {\n &.url {\n font-size: 14px;\n color: grey;\n margin-top: -12px;\n }\n}\n\nul ul { // toc\n padding-left: 20px;\n}\n\npre.highlight, pre.hljs {\n code {\n &.coffeescript, &.cs, &.javascript, &.js, &.ts, &.php {\n [class*=built_in] {\n color: lighten(#9AB4DB, 5%);\n }\n [class*=string] {\n color: lightgreen;\n }\n [class*=attribute] {\n color: white;\n }\n [class*=keyword] {\n color: lighten(brown, 30%);\n font-weight: 600;\n }\n [class*=literal] {\n color: lighten(red, 30%);\n }\n [class*=c1] {\n color: #B4B4B4;\n }\n [class*=constant] {\n color: #FFDF9D;\n }\n [class*=nx] {\n color: saturate(#9ECBEE, 20%);\n font-weight: 600;\n }\n }\n\n &.typescript {\n [class*=kr] {\n color: desaturate(yellow, 30%);\n }\n }\n\n &.ruby {\n .class .title, .class .parent {\n color: #FFDF9D;\n }\n\n [class*=keyword] {\n color: lighten(red, 25%);\n }\n [class*=kp] {\n color: lighten(red, 25%);\n }\n [class*=string] {\n color: lightgreen;\n }\n [class*=comment] {\n color: #B4B4B4;\n }\n [class*=constant] {\n color: #FFDF9D;\n }\n [class*=symbol] {\n color: #9ECBEE;\n }\n }\n }\n}\n\nimg[alt=glimmer_logo] {\n margin: 0 auto;\n width: 200px;\n display: block;\n margin-bottom: 4em;\n}\n\n.code-tabs .highlight {\n margin-top: 0;\n}\n\n\n.tabs {\n /*background: black;*/\n border-radius: 3px;\n\n .tab {\n background-color: #292E33;\n border: 1px solid black;\n font-weight: 500;\n color: lighten(#555, 20%);\n display: inline-block;\n padding: 0 8px 0 8px;\n margin-top: 10px;\n cursor: pointer;\n -webkit-transition: background-color .2s linear;\n -moz-transition: background-color .2s linear;\n -ms-transition: background-color .2s linear;\n -o-transition: background-color .2s linear;\n transition: background-color .2s linear;\n border-bottom: 1px solid black;\n font-size: 14px;\n\n &.active {\n color: white;\n text-shadow: 2px 2px black;\n font-weight: bold;\n /*background-color: #fff;*/\n border-radius: 5px 5px 0 0;\n }\n }\n}\n\n#next a, #next a small {\n color: white;\n}\n\n.highlight {\n $generic: #edf2f9;\n $comment: #9c9c9c;\n $yellow: #e6eb9d;\n $blue: #a2dcff;\n\n margin-top: 2rem;\n margin-bottom: 2rem;\n\n pre {\n code {\n font-size: 14px;\n line-height: 1.5;\n }\n }\n\n .language-ruby {\n .n {\n color: white;\n }\n\n .c1 {\n color: $comment;\n }\n\n .ss {\n color: $blue;\n font-weight: 600;\n }\n\n .k, .kp {\n color: #ff8080;\n }\n\n .p {\n color: white;\n }\n\n .nc, .no {\n color: $yellow;\n font-weight: 600;\n }\n\n .o, .si, .nf {\n color: white;\n }\n\n .s1, .s2, .dl {\n color: lightgreen;\n font-weight: 600;\n }\n\n .vi, .mi, .nb {\n color: lighten(#ff66d9, 10%);\n font-weight: 600;\n }\n }\n\n .language-http {\n color: $generic;\n }\n\n .language-typescript, .language-javascript {\n .kd {\n color: $generic;\n font-weight: 600;\n }\n\n .o {\n color: $generic;\n }\n\n .p {\n color: $generic;\n }\n\n .nx, .nd {\n color: $blue;\n font-weight: 600;\n }\n\n .nb {\n color: white;\n }\n\n .kc {\n color: $yellow;\n }\n\n .kr {\n color: lighten(#008080, 20%);\n }\n\n .c1 {\n color: #9c9c9c;\n }\n\n .k {\n color: #ff66d9;\n }\n\n .err {\n font-weight: 600;\n color: $yellow;\n }\n\n .na, .nl {\n color: #ff66d9;\n }\n\n .s1, .s2, .dl {\n color: lightgreen;\n font-weight: 600;\n }\n\n .mi {\n color: $yellow;\n }\n }\n\n .language-error {\n color: lighten(red, 20%);\n font-weight: 600;\n }\n\n .language-bash {\n color: white;\n }\n\n .language-javascript {\n .k {\n color: $yellow !important;\n }\n }\n}\n\n.home {\n $base: 16px;\n\n width: 95%;\n margin: 0 auto;\n text-align: center;\n font-size: $base;\n line-height: 1.4;\n padding-top: 10px;\n z-index: 20;\n\n @media (min-width: 569px) {\n font-size: 18px;\n }\n\n .skip-intro {\n position: absolute;\n right: 20px;\n padding: 3px 6px;\n font-size: 90%;\n }\n\n .start {\n margin-top: 40px;\n }\n\n // unused\n .img {\n position: absolute;\n background-repeat: no-repeat;\n opacity: 0.1;\n width: 100%;\n height: 100%;\n background-position: 70% 10%;\n background-size: 40%;\n }\n\n .sample {\n .highlight pre code {\n font-size: 14px !important;\n\n @media (min-width: 569px) {\n font-size: 18px !important;\n }\n }\n\n &.s {\n .highlight pre code {\n font-size: 11px !important;\n\n @media (min-width: 569px) {\n font-size: 18px !important;\n }\n }\n }\n\n figure {\n &:last-child {\n margin-bottom: 18px;\n }\n }\n }\n\n .mag {\n color: darken(magenta, 5%);\n }\n\n .lg {\n color: grey;\n }\n\n .p {\n margin-bottom: 2.5rem;\n }\n\n .bold {\n font-weight: bold;\n }\n\n .italic {\n font-style: italic;\n }\n\n .underline {\n text-decoration: underline;\n }\n\n .s {\n font-size: $base*1.2;\n }\n\n .m {\n font-size: $base*1.6;\n }\n\n .l {\n font-size: $base*2;\n }\n\n .xl {\n font-size: $base*2.2;\n }\n\n .quote {\n margin-top: 5px;\n font-size: $base*1.2;\n\n @media (min-width: 569px) {\n font-size: 24px !important;\n }\n\n &.yehuda {\n @media (min-width: 569px) {\n font-size: 30px !important;\n }\n }\n }\n\n .highlight {\n text-align: left;\n\n pre {\n padding-bottom: 0;\n }\n }\n}\n\n.blog {\n h2 a {\n color: white;\n\n &:hover {\n &:before {\n display: none;\n }\n }\n }\n}\n","/*!\n * Bootstrap v3.3.7 (http://getbootstrap.com)\n * Copyright 2011-2016 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -webkit-text-size-adjust: 100%;\n -ms-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: 1px dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n margin: .67em 0;\n font-size: 2em;\n}\nmark {\n color: #000;\n background: #ff0;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n position: relative;\n font-size: 75%;\n line-height: 0;\n vertical-align: baseline;\n}\nsup {\n top: -.5em;\n}\nsub {\n bottom: -.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n height: 0;\n -webkit-box-sizing: content-box;\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n margin: 0;\n font: inherit;\n color: inherit;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n padding: 0;\n border: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: content-box;\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n -webkit-appearance: textfield;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n padding: .35em .625em .75em;\n margin: 0 2px;\n border: 1px solid #c0c0c0;\n}\nlegend {\n padding: 0;\n border: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-spacing: 0;\n border-collapse: collapse;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n color: #000 !important;\n text-shadow: none !important;\n background: transparent !important;\n -webkit-box-shadow: none !important;\n box-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: 'Glyphicons Halflings';\n\n src: url('../../fonts/glyphicons-halflings-regular.eot');\n src: url('../../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\002a\";\n}\n.glyphicon-plus:before {\n content: \"\\002b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333;\n background-color: #fff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n display: inline-block;\n max-width: 100%;\n height: auto;\n padding: 4px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: all .2s ease-in-out;\n -o-transition: all .2s ease-in-out;\n transition: all .2s ease-in-out;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: normal;\n line-height: 1;\n color: #777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n padding: .2em;\n background-color: #fcf8e3;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n margin-left: -5px;\n list-style: none;\n}\n.list-inline > li {\n display: inline-block;\n padding-right: 5px;\n padding-left: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n overflow: hidden;\n clear: left;\n text-align: right;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted #777;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n text-align: right;\n border-right: 5px solid #eee;\n border-left: 0;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: '\\00A0 \\2014';\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #fff;\n background-color: #333;\n border-radius: 3px;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n color: #333;\n word-break: break-all;\n word-wrap: break-word;\n background-color: #f5f5f5;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n padding-right: 15px;\n padding-left: 15px;\n margin-right: auto;\n margin-left: auto;\n}\n.row {\n margin-right: -15px;\n margin-left: -15px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-right: 15px;\n padding-left: 15px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0;\n }\n}\n@media (min-width: 992px) {\n .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0;\n }\n}\ntable {\n background-color: transparent;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #ddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #ddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #ddd;\n}\n.table .table {\n background-color: #fff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #ddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\ntable col[class*=\"col-\"] {\n position: static;\n display: table-column;\n float: none;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n display: table-cell;\n float: none;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n min-height: .01%;\n overflow-x: auto;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #ddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n min-width: 0;\n padding: 0;\n margin: 0;\n border: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: bold;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555;\n background-color: #fff;\n background-image: none;\n border: 1px solid #ccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);\n box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6);\n}\n.form-control::-moz-placeholder {\n color: #999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999;\n}\n.form-control::-ms-expand {\n background-color: transparent;\n border: 0;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-top: 4px \\9;\n margin-left: -20px;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n vertical-align: middle;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.form-control-static {\n min-height: 34px;\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-right: 0;\n padding-left: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #3c763d;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #8a6d3b;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n background-color: #f2dede;\n border-color: #a94442;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n padding-top: 7px;\n margin-top: 0;\n margin-bottom: 0;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n padding-top: 7px;\n margin-bottom: 0;\n text-align: right;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 11px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n padding: 6px 12px;\n margin-bottom: 0;\n font-size: 14px;\n font-weight: normal;\n line-height: 1.42857143;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n -ms-touch-action: manipulation;\n touch-action: manipulation;\n cursor: pointer;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n outline: 0;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n filter: alpha(opacity=65);\n -webkit-box-shadow: none;\n box-shadow: none;\n opacity: .65;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333;\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n background-image: none;\n}\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus {\n background-color: #fff;\n border-color: #ccc;\n}\n.btn-default .badge {\n color: #fff;\n background-color: #333;\n}\n.btn-primary {\n color: #fff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #fff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #fff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #fff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n background-image: none;\n}\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.btn-success {\n color: #fff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #fff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #fff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #fff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n background-image: none;\n}\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #fff;\n}\n.btn-info {\n color: #fff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #fff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #fff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #fff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n background-image: none;\n}\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #fff;\n}\n.btn-warning {\n color: #fff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #fff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #fff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #fff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n background-image: none;\n}\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #fff;\n}\n.btn-danger {\n color: #fff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #fff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #fff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #fff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n background-image: none;\n}\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #fff;\n}\n.btn-link {\n font-weight: normal;\n color: #337ab7;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity .15s linear;\n -o-transition: opacity .15s linear;\n transition: opacity .15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-timing-function: ease;\n -o-transition-timing-function: ease;\n transition-timing-function: ease;\n -webkit-transition-duration: .35s;\n -o-transition-duration: .35s;\n transition-duration: .35s;\n -webkit-transition-property: height, visibility;\n -o-transition-property: height, visibility;\n transition-property: height, visibility;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n font-size: 14px;\n text-align: left;\n list-style: none;\n background-color: #fff;\n -webkit-background-clip: padding-box;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, .15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, .175);\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: 1.42857143;\n color: #333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n color: #262626;\n text-decoration: none;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #fff;\n text-decoration: none;\n background-color: #337ab7;\n outline: 0;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu-left {\n right: auto;\n left: 0;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n content: \"\";\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n right: 0;\n left: auto;\n }\n .navbar-right .dropdown-menu-left {\n right: auto;\n left: 0;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-right: 8px;\n padding-left: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-right: 12px;\n padding-left: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n display: table-cell;\n float: none;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-right: 0;\n padding-left: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group .form-control:focus {\n z-index: 3;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: normal;\n line-height: 1;\n color: #555;\n text-align: center;\n background-color: #eee;\n border: 1px solid #ccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n padding-left: 0;\n margin-bottom: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eee;\n}\n.nav > li.disabled > a {\n color: #777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777;\n text-decoration: none;\n cursor: not-allowed;\n background-color: transparent;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #ddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eee #eee #ddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555;\n cursor: default;\n background-color: #fff;\n border: 1px solid #ddd;\n border-bottom-color: transparent;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #fff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n margin-bottom: 5px;\n text-align: center;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #ddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #ddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #fff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n padding-right: 15px;\n padding-left: 15px;\n overflow-x: visible;\n -webkit-overflow-scrolling: touch;\n border-top: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1);\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-right: 0;\n padding-left: 0;\n }\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.navbar-brand {\n float: left;\n height: 50px;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n padding: 9px 10px;\n margin-top: 8px;\n margin-right: 15px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n padding: 10px 15px;\n margin-top: 8px;\n margin-right: -15px;\n margin-bottom: 8px;\n margin-left: -15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1);\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n padding-top: 0;\n padding-bottom: 0;\n margin-right: 0;\n margin-left: 0;\n border: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-right: 15px;\n margin-left: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n border-color: #ddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #ddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n color: #555;\n background-color: #e7e7e7;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #ccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-link {\n color: #777;\n}\n.navbar-default .navbar-link:hover {\n color: #333;\n}\n.navbar-default .btn-link {\n color: #777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #ccc;\n}\n.navbar-inverse {\n background-color: #222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #fff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #fff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #fff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n color: #fff;\n background-color: #080808;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #fff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #fff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #fff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n padding: 0 5px;\n color: #ccc;\n content: \"/\\00a0\";\n}\n.breadcrumb > .active {\n color: #777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n margin-left: -1px;\n line-height: 1.42857143;\n color: #337ab7;\n text-decoration: none;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-top-left-radius: 4px;\n border-bottom-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 4px;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 2;\n color: #23527c;\n background-color: #eee;\n border-color: #ddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 3;\n color: #fff;\n cursor: default;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777;\n cursor: not-allowed;\n background-color: #fff;\n border-color: #ddd;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-top-left-radius: 6px;\n border-bottom-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-top-right-radius: 6px;\n border-bottom-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-top-left-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-top-right-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n text-align: center;\n list-style: none;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777;\n cursor: not-allowed;\n background-color: #fff;\n}\n.label {\n display: inline;\n padding: .2em .6em .3em;\n font-size: 75%;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: .25em;\n}\na.label:hover,\na.label:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n line-height: 1;\n color: #fff;\n text-align: center;\n white-space: nowrap;\n vertical-align: middle;\n background-color: #777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #fff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n padding-right: 15px;\n padding-left: 15px;\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-right: 60px;\n padding-left: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #fff;\n border: 1px solid #ddd;\n border-radius: 4px;\n -webkit-transition: border .2s ease-in-out;\n -o-transition: border .2s ease-in-out;\n transition: border .2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-right: auto;\n margin-left: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@-o-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n height: 20px;\n margin-bottom: 20px;\n overflow: hidden;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);\n}\n.progress-bar {\n float: left;\n width: 0;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #fff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15);\n -webkit-transition: width .6s ease;\n -o-transition: width .6s ease;\n transition: width .6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n -webkit-background-size: 40px 40px;\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n overflow: hidden;\n zoom: 1;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n padding-left: 0;\n margin-bottom: 20px;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #fff;\n border: 1px solid #ddd;\n}\n.list-group-item:first-child {\n border-top-left-radius: 4px;\n border-top-right-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n color: #555;\n text-decoration: none;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n color: #777;\n cursor: not-allowed;\n background-color: #eee;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #fff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, .05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #ddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-right: 15px;\n padding-left: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #ddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n margin-bottom: 0;\n border: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #ddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #ddd;\n}\n.panel-default {\n border-color: #ddd;\n}\n.panel-default > .panel-heading {\n color: #333;\n background-color: #f5f5f5;\n border-color: #ddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #fff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #fff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, .15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000;\n text-shadow: 0 1px 0 #fff;\n filter: alpha(opacity=20);\n opacity: .2;\n}\n.close:hover,\n.close:focus {\n color: #000;\n text-decoration: none;\n cursor: pointer;\n filter: alpha(opacity=50);\n opacity: .5;\n}\nbutton.close {\n -webkit-appearance: none;\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n display: none;\n overflow: hidden;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transition: -webkit-transform .3s ease-out;\n -o-transition: -o-transform .3s ease-out;\n transition: transform .3s ease-out;\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #fff;\n -webkit-background-clip: padding-box;\n background-clip: padding-box;\n border: 1px solid #999;\n border: 1px solid rgba(0, 0, 0, .2);\n border-radius: 6px;\n outline: 0;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, .5);\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000;\n}\n.modal-backdrop.fade {\n filter: alpha(opacity=0);\n opacity: 0;\n}\n.modal-backdrop.in {\n filter: alpha(opacity=50);\n opacity: .5;\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-bottom: 0;\n margin-left: 5px;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, .5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 12px;\n font-style: normal;\n font-weight: normal;\n line-height: 1.42857143;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n filter: alpha(opacity=0);\n opacity: 0;\n\n line-break: auto;\n}\n.tooltip.in {\n filter: alpha(opacity=90);\n opacity: .9;\n}\n.tooltip.top {\n padding: 5px 0;\n margin-top: -3px;\n}\n.tooltip.right {\n padding: 0 5px;\n margin-left: 3px;\n}\n.tooltip.bottom {\n padding: 5px 0;\n margin-top: 3px;\n}\n.tooltip.left {\n padding: 0 5px;\n margin-left: -3px;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #fff;\n text-align: center;\n background-color: #000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-left .tooltip-arrow {\n right: 5px;\n bottom: 0;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n font-style: normal;\n font-weight: normal;\n line-height: 1.42857143;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n letter-spacing: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n white-space: normal;\n background-color: #fff;\n -webkit-background-clip: padding-box;\n background-clip: padding-box;\n border: 1px solid #ccc;\n border: 1px solid rgba(0, 0, 0, .2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, .2);\n\n line-break: auto;\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover-title {\n padding: 8px 14px;\n margin: 0;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow:after {\n content: \"\";\n border-width: 10px;\n}\n.popover.top > .arrow {\n bottom: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-color: #999;\n border-top-color: rgba(0, 0, 0, .25);\n border-bottom-width: 0;\n}\n.popover.top > .arrow:after {\n bottom: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-color: #fff;\n border-bottom-width: 0;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-right-color: #999;\n border-right-color: rgba(0, 0, 0, .25);\n border-left-width: 0;\n}\n.popover.right > .arrow:after {\n bottom: -10px;\n left: 1px;\n content: \" \";\n border-right-color: #fff;\n border-left-width: 0;\n}\n.popover.bottom > .arrow {\n top: -11px;\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999;\n border-bottom-color: rgba(0, 0, 0, .25);\n}\n.popover.bottom > .arrow:after {\n top: 1px;\n margin-left: -10px;\n content: \" \";\n border-top-width: 0;\n border-bottom-color: #fff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999;\n border-left-color: rgba(0, 0, 0, .25);\n}\n.popover.left > .arrow:after {\n right: 1px;\n bottom: -10px;\n content: \" \";\n border-right-width: 0;\n border-left-color: #fff;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n width: 100%;\n overflow: hidden;\n}\n.carousel-inner > .item {\n position: relative;\n display: none;\n -webkit-transition: .6s ease-in-out left;\n -o-transition: .6s ease-in-out left;\n transition: .6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform .6s ease-in-out;\n -o-transition: -o-transform .6s ease-in-out;\n transition: transform .6s ease-in-out;\n\n -webkit-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n left: 0;\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n left: 0;\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n left: 0;\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 15%;\n font-size: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, .6);\n background-color: rgba(0, 0, 0, 0);\n filter: alpha(opacity=50);\n opacity: .5;\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%);\n background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001)));\n background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control.right {\n right: 0;\n left: auto;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%);\n background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5)));\n background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n background-repeat: repeat-x;\n}\n.carousel-control:hover,\n.carousel-control:focus {\n color: #fff;\n text-decoration: none;\n filter: alpha(opacity=90);\n outline: 0;\n opacity: .9;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n margin-top: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n font-family: serif;\n line-height: 1;\n}\n.carousel-control .icon-prev:before {\n content: '\\2039';\n}\n.carousel-control .icon-next:before {\n content: '\\203a';\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n padding-left: 0;\n margin-left: -30%;\n text-align: center;\n list-style: none;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n border: 1px solid #fff;\n border-radius: 10px;\n}\n.carousel-indicators .active {\n width: 12px;\n height: 12px;\n margin: 0;\n background-color: #fff;\n}\n.carousel-caption {\n position: absolute;\n right: 15%;\n bottom: 20px;\n left: 15%;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #fff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, .6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -10px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -10px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -10px;\n }\n .carousel-caption {\n right: 20%;\n left: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-header:before,\n.modal-header:after,\n.modal-footer:before,\n.modal-footer:after {\n display: table;\n content: \" \";\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-header:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-right: auto;\n margin-left: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */\n","@media (min-width: 1200px) {\n .container {\n width: 970px;\n }\n}\n\na {\n color: #459CE7;\n}\n\n.pagination>.active {\n >a,\n >span, \n >a:hover, \n >span:hover, \n >a:focus, \n >span:focus { \n background-color: #5585B3;\n border-color: #4477A3;\n }\n}\n.pagination>li>a, \n.pagination>li>span {\n @include transition(all 0.25s linear);\n}","body {\n font-family: $roboto;\n @include font-smoothing;\n}\n\na, button {\n &:focus {\n outline: none;\n }\n}\n\n.navbar {\n &.normal {\n min-height: 70px;\n background: #223741;\n border-radius: 0px;\n box-shadow: 0px 1px 3px 1px rgba(46,46,46,0.3), inset rgba(255, 255, 255, 0.2) 0 1px 1px;\n border: none;\n z-index: 9999;\n\n .navbar-header {\n .navbar-brand {\n color: #fff;\n padding: 25px 15px;\n font-weight: 400;\n font-size: 23px;\n }\n\n .navbar-toggle {\n margin-top: 17px;\n }\n }\n\n .navbar-collapse {\n @media(min-width: 768px) {\n text-align: center;\n }\n\n .navbar-nav {\n @media(min-width: 768px) {\n float: none;\n display: inline-block;\n }\n\n > li {\n float: none;\n display: inline-block;\n\n @media(max-width: 767px) {\n display: block;\n }\n\n &.active {\n > a {\n background: none;\n color: #fff;\n }\n }\n\n > a {\n padding: 25px 20px;\n color: #d5d5d5;\n font-weight: 500;\n z-index: 1001;\n\n @include transition(color 0.2s linear);\n\n @media(max-width: 767px) {\n padding: 18px 20px;\n }\n\n &:hover {\n color: #fff;\n }\n }\n \n &.open {\n ul.dropdown-menu {\n a {\n @media(max-width: 767px) {\n color: #fff;\n padding: 12px 18px 12px 30px;\n }\n }\n }\n }\n\n ul.dropdown-menu {\n text-align: left;\n padding: 9px 0;\n\n li a {\n padding: 8px 18px;\n }\n }\n }\n\n .button {\n padding: 8px 18px;\n font-size: 13px;\n position: relative;\n top: 17px;\n\n @media(max-width: 767px) {\n box-shadow: none;\n background: none;\n top: 0;\n margin-bottom: 20px;\n color: #d5d5d5 !important;\n font-size: 15px;\n }\n }\n }\n }\n }\n\n &.hero {\n margin: 0px;\n border-radius: 0px; \n border: 0px;\n z-index: 999;\n position: absolute;\n width: 100%;\n top: 0;\n \n @media (min-width: 768px) {\n background: transparent;\n padding-top: 10px;\n\n .navbar-nav >.active > a {\n background: transparent;\n color: #fff;\n }\n }\n \n .navbar-brand {\n color: #fff;\n font-weight: 400;\n font-size: 26px;\n }\n\n .navbar-nav {\n > li {\n &.dropdown > a .caret {\n border-top-color: #EBEBEB;\n }\n \n > a {\n color: #fff;\n font-size: 15px;\n padding: 15px 20px;\n font-weight: 500;\n\n @include transition(color 0.2s linear);\n\n &:hover {\n color: #fff;\n }\n }\n\n &.open {\n > a {\n color: #fff !important;\n background-color: rgba(0, 0, 0, 0.8);\n border-radius: 4px 4px 0 0;\n }\n\n ul.dropdown-menu {\n a {\n @media(max-width: 767px) {\n color: #fff;\n padding: 12px 18px 12px 30px;\n }\n }\n }\n }\n\n ul.dropdown-menu {\n text-align: left;\n background: rgba(0, 0, 0, 0.8);\n box-shadow: none;\n padding: 5px 0 8px 0;\n border-radius: 4px 0 4px 4px;\n border: 0px;\n\n li a {\n color: #fff;\n padding: 10px 18px;\n\n &:hover {\n color: #e9e9e9;\n background: none;\n }\n }\n }\n }\n }\n }\n\n &.white {\n min-height: 70px;\n margin-bottom: 0;\n border-radius: 0;\n border: 0;\n z-index: 999;\n\n @media (min-width: 768px) {\n background: #fff;\n box-shadow: 0px 1px 5px rgba(0, 0, 0, .75);\n }\n\n .container {\n @media (min-width: 1200px) {\n width: 1000px;\n }\n }\n\n .navbar-header {\n .navbar-brand {\n color: #333;\n font-weight: 400;\n font-size: 26px;\n padding-top: 24px;\n\n @media (max-width: 767px) {\n color: #fff;\n font-size: 20px;\n }\n }\n\n .navbar-toggle {\n margin-top: 17px;\n }\n }\n\n .navbar-collapse {\n\n @media(min-width: 768px) {\n text-align: center;\n }\n\n .navbar-nav {\n @media(min-width: 768px) {\n float: none;\n display: inline-block;\n }\n\n > li {\n float: none;\n display: inline-block;\n\n @media(max-width: 767px) {\n display: block;\n }\n\n &.active {\n > a {\n background: none;\n color: #333;\n }\n }\n\n > a {\n padding: 25px 20px;\n color: #555;\n z-index: 1001;\n\n @include transition(color 0.2s linear);\n\n &.signup {\n border: 1px solid #CED7DF;\n border-radius: 5px;\n padding: 8px 11px;\n font-size: 13px;\n color: #333;\n margin-left: 20px;\n }\n\n @media(max-width: 767px) {\n padding: 18px 20px;\n color: #fff;\n }\n\n .fa-chevron-down {\n color: #777;\n font-size: 8px;\n margin-left: 2px;\n position: relative;\n top: -1px;\n }\n\n &:hover {\n color: #333;\n\n @media(max-width: 767px) {\n color: #fff;\n }\n }\n }\n \n &.open {\n > a {\n @media(min-width: 768px) {\n background: #fff;\n color: #333;\n }\n\n .caret {\n border-top-color: #999;\n border-bottom-color: #999;\n }\n }\n\n ul.dropdown-menu {\n a {\n @media(max-width: 767px) {\n color: #fff;\n padding: 12px 18px 12px 30px;\n }\n }\n }\n }\n\n ul.dropdown-menu {\n text-align: left;\n border: 0;\n padding: 0 0 10px;\n box-shadow: 0 8px 12px rgba(0, 0, 0, 0.175);\n\n li a {\n padding: 8px 18px;\n }\n }\n }\n }\n }\n }\n}\n\n.main-footer {\n background: #414B5B;\n margin-top: 120px;\n padding-top: 60px;\n padding-bottom: 30px;\n\n @media(max-width: 991px) {\n padding-top: 30px;\n }\n\n .menu {\n @media(max-width: 767px) {\n text-align: center;\n margin-top: 40px;\n }\n\n h3 {\n margin-top: 0;\n color: #ced5e0;\n text-transform: uppercase;\n letter-spacing: 4px;\n font-size: 14px;\n font-weight: 600;\n }\n\n ul {\n list-style-type: none;\n padding: 0;\n margin-top: 30px;\n\n li {\n display: block;\n margin-bottom: 9px;\n\n a {\n color: #fff;\n display: inline-block;\n font-weight: 500;\n font-size: 13px;\n\n @include transition(all 0.2s linear);\n\n &:hover {\n color: #f7f7f7;\n text-decoration: none;\n }\n\n @media(max-width: 991px) {\n padding: 0;\n }\n }\n\n .hiring {\n font-weight: 700;\n letter-spacing: .3px;\n text-transform: uppercase;\n word-spacing: 1.5px;\n font-size: 11px;\n margin-left: 4px;\n padding: 3px 8px;\n border-radius: 4px;\n color: #fff;\n background: #49B7E2;\n\n &:hover {\n color: #fff;\n background: #0E82AF;\n }\n }\n }\n }\n }\n\n .newsletter {\n text-align: right;\n\n @media(max-width: 767px) {\n text-align: center;\n margin-top: 40px;\n }\n\n .signup {\n margin-bottom: 40px;\n\n p {\n font-size: 13px;\n color: #fff;\n width: 90%;\n float: right;\n text-align: left;\n line-height: 20px;\n\n @media(max-width: 767px) {\n float: none;\n text-align: center;\n width: 100%;\n }\n }\n\n form {\n float: right;\n width: 90%;\n position: relative;\n\n @media(max-width: 767px) {\n float: none;\n width: 100%;\n }\n\n input[type=\"text\"] {\n font-size: 13px;\n\n @include placeholder {\n font-size: 13px;\n line-height: 17px;\n }\n }\n\n input[type=\"submit\"] {\n border: 0;\n background: #84B0E2;\n color: #FFF;\n border-radius: 3px;\n padding: 6px 10px;\n position: absolute;\n top: 5px;\n text-shadow: 1px 1px rgba(0, 0, 0, 0.28);\n right: 5px;\n font-size: 13px;\n\n @include transition(all .2s linear);\n\n &:hover {\n background: #6284AC;\n }\n }\n }\n }\n\n a {\n border: 0;\n text-decoration: none;\n opacity: 1;\n filter: alpha(opacity=100);\n margin-left: 15px;\n position: relative;\n top: -10px;\n\n @include transition(all .3s ease);\n\n &:hover {\n opacity: 0.7;\n filter: alpha(opacity=70);\n }\n }\n }\n\n .credits {\n margin-top: 30px;\n\n .col-md-12 {\n padding-top: 20px;\n font-size: 13px;\n text-align: center;\n color: #a3aab5;\n }\n }\n\n &--white {\n background: #fff;\n margin-top: 0;\n\n .menu {\n h3 {\n color: #888;\n }\n\n ul {\n li {\n a {\n color: #89A1C0;\n\n &:hover {\n color: #000;\n }\n }\n }\n }\n }\n\n .newsletter {\n .signup {\n p {\n color: #666;\n }\n }\n }\n\n .credits {\n .col-md-12 {\n color: #A1AAB6;\n }\n }\n }\n}\n",".features-section {\n margin-top: 90px;\n\n &--margin {\n margin-top: 120px;\n\n .feature {\n margin-bottom: 110px;\n\n &.last {\n margin-bottom: 50px;\n }\n }\n }\n\n @media(max-width: 991px) {\n margin-top: 50px;\n }\n\n .header {\n text-align: center;\n\n h2 {\n color: #555;\n font-weight: 400;\n font-size: 27px;\n\n @media (max-width: 767px) {\n font-size: 25px;\n }\n }\n\n p {\n font-size: 17px;\n color: #757575;\n }\n }\n\n .feature {\n margin-top: 60px;\n\n &.last {\n .image img {\n margin-left: 70px;\n }\n }\n\n @media (min-width: 992px) {\n &.backwards {\n .info {\n float: right;\n }\n\n .image {\n float: left;\n\n img {\n margin: 0 auto;\n }\n }\n }\n }\n\n @media (max-width: 991px) {\n margin-top: 10px;\n }\n\n .info {\n h4 {\n color: #3B88B6;\n font-weight: normal;\n font-size: 19px;\n line-height: 25px;\n margin-top: 35px;\n\n @media (max-width: 991px) {\n text-align: center;\n }\n }\n\n p {\n color: #6C7279;\n font-size: 14px;\n margin-top: 20px;\n line-height: 24px;\n width: 83%;\n font-weight: normal;\n\n @media (max-width: 991px) {\n text-align: center;\n width: 100%;\n font-size: 14px;\n }\n }\n }\n\n .image {\n @media (max-width: 991px) {\n img {\n margin: 0 auto !important;\n margin-top: 20px;\n }\n }\n }\n }\n\n .divider {\n height: 1px;\n width: 100%;\n background-color: #ddd;\n position: relative;\n margin: 70px 0 60px;\n\n &:before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 2px;\n background-image: -webkit-gradient(linear, left top, right top, color-stop(0, rgba(255, 255, 255, 0.75)), color-stop(0.5, rgba(250,250,250,0)), color-stop(1, rgba(255, 255, 255, 0.75)));\n background-image: -webkit-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: -moz-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: -o-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: -ms-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n }\n\n @media (max-width: 991px) {\n margin: 55px 0 35px;\n }\n }\n}\n",".slider-section {\n margin-top: 70px;\n\n @media (max-width: 991px) {\n margin-top: 30px;\n }\n\n .header {\n text-align: center;\n\n h3 {\n font-size: 25px;\n color: #555;\n line-height: 25px;\n font-weight: 400;\n margin-bottom: 15px;\n }\n }\n\n .slide-wrapper {\n border-bottom: 1px solid #E4E4E4;\n\n .slideshow {\n background: url(\"../../images/slider-bg.png\") 0 0 no-repeat;\n margin: 0 auto;\n margin-top: 35px;\n width: 844px;\n height: 418px;\n position: relative;\n overflow: hidden;\n\n @media(max-width: 991px) {\n background: none;\n width: inherit;\n height: 340px;\n }\n\n @media(max-width: 620px) {\n height: 290px;\n }\n\n @media(max-width: 530px) {\n height: 240px;\n }\n\n @media(max-width: 450px) {\n height: 205px;\n }\n\n .btn-nav {\n width: 48px;\n height: 51px;\n position: absolute;\n top: 55%;\n z-index: 999;\n cursor: pointer;\n opacity: 0;\n filter: alpha(opacity=0);\n -webkit-transition: all .3s ease;\n -moz-transition: all .3s ease;\n -o-transition: all .3s ease;\n -ms-transition: all .3s ease;\n transition: all .3s ease;\n\n &.active {\n opacity: 1;\n filter: alpha(opacity=100);\n @include transform(translateX(0px) !important);\n }\n\n &.prev {\n background: url(\"../../images/slider/slide-left.png\") 0 0 no-repeat;\n left: 91px;\n @include transform(translateX(-48px));\n\n @media(max-width: 991px) {\n left: 30px;\n }\n }\n \n &.next {\n background: url(\"../../images/slider/slide-right.png\") 0 0 no-repeat;\n right: 91px;\n @include transform(translateX(48px));\n\n @media(max-width: 991px) {\n right: 29px;\n }\n }\n } \n\n .slide {\n width: 100%;\n height: 100%;\n position: absolute; \n\n &.active {\n img {\n opacity: 1;\n filter: alpha(opacity=100);\n @include transform(translateY(0px));\n }\n }\n\n img {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n margin: auto;\n\n @include transform(translateY(300px));\n opacity: 0;\n -ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);\n filter: alpha(opacity=0);\n -webkit-transition: all .6s ease;\n -moz-transition: all .6s ease;\n -o-transition: all .6s ease;\n -ms-transition: all .6s ease;\n transition: all .6s ease;\n\n @media(max-width: 767px) {\n width: 100%;\n }\n }\n }\n } \n }\n}",".index-hero {\n overflow: hidden;\n background: #4B4848;\n background-size: cover;\n background-image: url(\"../../images/8254861601_1fbf969a14_b.jpg\");\n /*background: -webkit-radial-gradient(closest-corner, rgba(16, 47, 70, 0) 60%, rgba(16, 47, 70, 0.26)), -webkit-linear-gradient(108deg, #5E8199, #153752 90%);*/\n\n background-position: center 16%;\n height: 580px;\n position: relative;\n padding-top: 150px;\n box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.4);\n\n @media (max-width: 991px) {\n padding-top: 100px;\n }\n\n @media (max-width: 767px) {\n height: 550px;\n }\n\n &:before {\n position: absolute;\n content: '';\n left: 0;\n bottom: 0;\n width: 100%;\n height: 100%;\n background: -webkit-linear-gradient(right, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0) 1%, rgba(0, 0, 0, 0.1) 26%, rgba(0, 0, 0, 0.35) 71%, rgba(0, 0, 0, 0.5) 100%);\n background: -moz-linear-gradient(right, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0) 1%, rgba(0, 0, 0, 0.1) 26%, rgba(0, 0, 0, 0.35) 71%, rgba(0, 0, 0, 0.5) 100%);\n background: -o-linear-gradient(right, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0) 1%, rgba(0, 0, 0, 0.1) 26%, rgba(0, 0, 0, 0.35) 71%, rgba(0, 0, 0, 0.5) 100%);\n background: linear-gradient(right, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0) 1%, rgba(0, 0, 0, 0.1) 26%, rgba(0, 0, 0, 0.35) 71%, rgba(0, 0, 0, 0.5) 100%);\n }\n\n &:after {\n position: absolute;\n content: '';\n left: 0;\n top: 0;\n width: 100%;\n height: 100%;\n background: rgba(32, 54, 114, 0.29);\n }\n \n .container {\n position: relative;\n z-index: 33;\n }\n\n h1.hero-text {\n line-height: 52px;\n text-align: left;\n font-size: 37px;\n text-transform: uppercase;\n font-weight: 300;\n letter-spacing: .3px;\n color: #FFF;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);\n\n -webkit-animation-duration: 1s;\n\n @media (max-width: 767px) {\n font-size: 25px;\n line-height: 38px;\n margin-top: 0;\n text-align: center;\n }\n }\n\n .sub-text {\n width: 50%;\n margin-top: 25px;\n color: #fff;\n font-weight: 400;\n font-size: 17px;\n line-height: 28px;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);\n\n -webkit-animation-duration: 1s;\n\n @media (max-width: 991px) {\n width: 70%;\n }\n\n @media (max-width: 767px) {\n font-size: 15px;\n width: 100%;\n text-align: center;\n }\n }\n\n .cta {\n @include animationDelay(0.6s);\n margin-top: 60px;\n\n @media (max-width: 767px) {\n text-align: center;\n }\n\n a {\n margin-right: 20px;\n padding: 15px 31px;\n\n @media (max-width: 767px) {\n font-size: 14px;\n padding: 12px 25px;\n margin: 0px 15px 15px 0px;\n }\n }\n }\n\n .img {\n position: absolute;\n top: 15px;\n left: 625px;\n width: 570px;\n height: 333px;\n background-image: url(\"../../images/static-hero.png\");\n background-repeat: no-repeat;\n\n @media(max-width: 991px) {\n display: none;\n }\n }\n}\n\n.testimonials-section {\n margin-top: 100px;\n\n .header {\n text-align: center;\n\n h3 {\n font-size: 23px;\n color: #656565;\n font-weight: 400;\n line-height: 25px;\n margin-bottom: 45px;\n }\n }\n\n .testimonial {\n width: 90%;\n\n @media (max-width: 767px) {\n float: none !important;\n margin: 0 auto;\n margin-bottom: 45px;\n }\n\n .quote { \n color: #53565f;\n line-height: 26px;\n border: 1px solid #E2E2E2;\n padding: 16px 25px;\n border-radius: 5px;\n width: 95%;\n position: relative;\n\n @media (max-width: 767px) {\n margin: 0 auto;\n }\n\n .arrow-down {\n position: absolute;\n bottom: 3px;\n left: 30px;\n\n .arrow, .arrow-border {\n border-color: #fff transparent transparent;\n border-style: solid;\n border-width: 11px;\n cursor: pointer;\n position: absolute;\n top: 3px;\n z-index: 1002;\n }\n\n .arrow-border {\n border-color: #E2E2E2 transparent transparent;\n border-width: 12px;\n top: 3px;\n z-index: 1001;\n left: -1px;\n }\n }\n }\n\n .author {\n margin-top: 40px;\n margin-left: 10px;\n\n @media (max-width: 767px) {\n margin-left: 6%;\n }\n\n .pic {\n width: 71px;\n height: 71px;\n border-radius: 50px;\n float: left;\n position: relative;\n top: -12px;\n margin-right: 18px;\n border: 1px solid #ccc;\n }\n\n .name {\n color: #3C92C5;\n line-height: 23px;\n font-weight: 500;\n }\n\n .company {\n font-size: 14px;\n color: #909090;\n line-height: 23px;\n font-weight: 400;\n }\n }\n }\n}\n\n.cta-section {\n text-align: center;\n margin-top: 120px;\n\n p {\n font-size: 18px;\n color: #7A7D81;\n letter-spacing: 0.1px;\n }\n\n a {\n display: inline-block;\n margin-top: 30px;\n background: #6CAAE4;\n color: #FFF;\n padding: 23px 40px;\n font-size: 16px;\n letter-spacing: 1px;\n font-weight: 500;\n text-transform: uppercase;\n border-radius: 5px;\n box-shadow: rgba(23, 43, 99, 0.3) 0 7px 28px;\n\n @include transition(all .2s linear);\n\n &:hover {\n text-decoration: none;\n background: #3FD8DF;\n }\n }\n}\n\n.clients-section {\n background: #5E7A9B;\n background: radial-gradient(#36414E, #253241);\n margin-top: 120px;\n padding: 65px 0 80px;\n text-align: center;\n\n h3 {\n color: #FFF;\n margin-top: 0;\n font-weight: 300;\n font-size: 35px;\n letter-spacing: .3px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n }\n\n p {\n font-size: 16px;\n color: #f8f8f8;\n margin-top: 17px;\n font-weight: 300;\n line-height: 25px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n }\n\n .logos {\n margin-top: 35px;\n\n img {\n margin: 0 16px;\n\n @media(max-width: 767px) {\n margin: 0 auto;\n display: block;\n margin-bottom: 15px;\n }\n }\n }\n}\n","@include keyFrame(slideLeft) {\n 0% {\n left: 0;\n }\n 100% {\n left: -15px;\n }\n}\n\n.slider-hero {\n background: #4B4848;\n height: 585px;\n position: relative;\n\n @media (max-width: 991px) {\n padding-top: 100px;\n }\n\n @media (max-width: 767px) {\n height: 550px;\n }\n\n /* prev & next arrows */\n .slide-nav {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 140px;\n z-index: 200;\n text-indent: 100%;\n overflow: hidden;\n opacity: 0.5;\n filter:alpha(opacity=50);\n\n @include transition(opacity .2s linear);\n\n &:hover {\n opacity: 1;\n filter:alpha(opacity=100);\n }\n\n &.prev {\n &:after {\n content: \"\";\n display: block;\n position: absolute;\n top: 0;\n bottom: 0;\n margin: auto 0;\n width: 19px;\n height: 58px;\n background: url(\"../../images/slide-arrow.png\") no-repeat;\n left: 30px;\n\n @include transform(rotate(180deg));\n }\n }\n\n &.next {\n right: 0;\n\n &:after {\n content: \"\";\n display: block;\n position: absolute;\n top: 0;\n bottom: 0;\n margin: auto 0;\n width: 19px;\n height: 58px;\n background: url(\"../../images/slide-arrow.png\") no-repeat;\n right: 30px;\n }\n }\n }\n\n /* navigation dots */\n nav {\n z-index: 999;\n position: absolute;\n bottom: 20px;\n width: 100%;\n text-align: center;\n\n a {\n display: inline-block;\n width: 10px;\n height: 10px;\n background: #a2a2a2;\n border-radius: 50%;\n margin: 0 3px;\n \n &.active {\n background: #fff;\n }\n }\n }\n\n /* slides */\n .slides {\n overflow: hidden;\n position: absolute;\n top: 0px;\n width: 100%;\n height: 100%;\n\n /* each slide */\n .slide {\n display: none;\n z-index: 80;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n width: 100%;\n padding-top: 90px;\n \n &.active {\n opacity: 1;\n filter: alpha(opacity=100);\n display: block;\n z-index: 100;\n }\n\n &.next {\n .bg {\n @media(min-width: 768px) {\n @include animation(slideLeft 700ms linear); \n }\n }\n\n z-index: 90;\n }\n\n .container {\n position: relative;\n z-index: 3;\n height: 100%;\n }\n\n .bg {\n position: absolute;\n top: 0;\n left: -15px;\n width: 102%;\n height: 100%;\n background-position: center center;\n background-size: cover;\n\n @media(max-width: 767px) {\n left: 0px;\n }\n\n &:before {\n position: absolute;\n top: 10%;\n bottom: 0;\n left: 0;\n right: 0;\n content: '';\n background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(0,0,0,0)), color-stop(100%, rgba(0,0,0,0.25)));\n background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(0,0,0,0.25) 100%);\n background: -moz-linear-gradient(top, rgba(0,0,0,0),rgba(0,0,0,0.25));\n background: -o-linear-gradient(top, rgba(0,0,0,0),rgba(0,0,0,0.25));\n background: linear-gradient(top, rgba(0,0,0,0),rgba(0,0,0,0.25));\n }\n\n &:after {\n content: '';\n position: absolute;\n display: block;\n height: 100%;\n left: 0;\n width: 100%;\n top: 0;\n }\n }\n\n &.first {\n @media(max-width: 767px) {\n padding-top: 40px;\n }\n\n .bg {\n background-image: url('../../images/bgs/photo-1467703834117-04386e3dadd8.jpeg');\n background-position: center 29%;\n\n &:after {\n background: rgba(5, 11, 29, 0.4);\n }\n }\n\n h1.hero-text {\n text-align: center;\n font-family: $lato;\n font-size: 37px;\n font-weight: 400;\n color: #fff;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.65);\n\n @media (max-width: 767px) {\n font-size: 25px;\n }\n }\n\n .sub-text {\n margin: 0 auto;\n font-family: $lato;\n line-height: 26px;\n margin-top: 25px;\n text-align: center;\n color: #fff;\n font-weight: 400;\n font-size: 17px;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2);\n width: 50%;\n\n @include animationDelay(0.6s);\n\n @media (max-width: 991px) {\n width: 70%;\n }\n\n @media (max-width: 767px) {\n font-size: 15px;\n }\n }\n\n .video-wrapper {\n text-align: center;\n margin-top: 50px;\n \n @media(max-width: 767px) {\n margin-top: 25px;\n }\n\n .video {\n display: inline-block;\n padding: 6px;\n background: rgba(255, 255, 255, 0.5);\n border-radius: 5px;\n -webkit-animation-duration: 1.2s;\n -moz-animation-duration: 1.2s;\n -o-animation-duration: 1.2s;\n animation-duration: 1.2s;\n \n img {\n max-width: 280px;\n cursor: pointer;\n\n @media(max-width: 767px) {\n max-width: 200px;\n }\n }\n }\n }\n }\n\n &.second {\n @media(max-width: 767px) {\n padding-top: 50px;\n }\n\n .bg {\n background-image: url(\"../../images/bgs/photo-1472132858735-6313c7962473.jpeg\");\n background-position: center 18%;\n\n &:after {\n background: rgba(52, 52, 58, 0.5);\n }\n }\n\n .info {\n h1.hero-text {\n margin-top: 65px;\n font-family: $lato;\n font-size: 40px;\n line-height: 45px;\n font-weight: 400;\n color: #fff;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);\n\n @media (max-width: 767px) {\n font-size: 28px;\n text-align: center;\n }\n }\n\n p {\n color: rgba(255, 255, 255, 0.94);\n font-family: $lato;\n letter-spacing: 0.3px;\n margin-top: 25px;\n font-size: 17px;\n line-height: 26px;\n }\n\n .cta {\n @include animationDelay(0.6s);\n margin-top: 40px;\n\n @media (max-width: 767px) {\n text-align: center;\n }\n\n a {\n position: relative;\n margin: 0px 30px 0 0;\n padding: 16px 25px;\n letter-spacing: 1px;\n font-weight: 500;\n text-transform: uppercase;\n border: 2px solid rgba(255, 255, 255, 0.8);\n background: rgba(0, 0, 0, 0.4);\n\n &:hover {\n color: #252525;\n background: #fff;\n padding-right: 45px;\n\n .fa {\n opacity: 1;\n }\n }\n\n .fa {\n opacity: 0;\n font-size: 13px;\n position: absolute;\n color: #252525;\n top: 21px;\n right: 20px;\n\n @include transition(all 0.15s linear);\n }\n\n @media (max-width: 767px) {\n font-size: 14px;\n padding: 12px 25px;\n margin: 0px 15px 15px 0px;\n }\n }\n }\n }\n\n .mobiles {\n img {\n display: block;\n margin: 0 auto;\n width: 360px;\n -webkit-animation-duration: 1.2s;\n -moz-animation-duration: 1.2s;\n -o-animation-duration: 1.2s;\n animation-duration: 1.2s;\n }\n }\n }\n\n &.third {\n padding-top: 115px;\n\n @media(max-width: 767px) {\n padding-top: 50px;\n }\n\n .bg {\n background-image: url('../../images/bgs/photo-1467659226669-a1360d97be2d.jpeg');\n background-position: center 15%;\n\n &:after {\n background: rgba(3, 9, 27, 0.5);\n }\n }\n\n h1, .sub-text {\n -webkit-animation-duration: 1s;\n -moz-animation-duration: 1s;\n -o-animation-duration: 1s;\n animation-duration: 1s;\n }\n\n h1.hero-text {\n text-align: center;\n font-size: 38px;\n font-weight: 400;\n color: #fff;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);\n\n @media (max-width: 767px) {\n font-size: 28px;\n }\n }\n\n .sub-text {\n margin: 0 auto;\n margin-top: 25px;\n text-align: center;\n color: #f7f7f7;\n font-weight: 400;\n font-size: 17px;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2);\n width: 50%;\n\n @media (max-width: 991px) {\n width: 70%;\n }\n\n @media (max-width: 767px) {\n font-size: 15px;\n }\n }\n\n .cta {\n @include animationDelay(0.6s);\n text-align: center;\n margin-top: 60px;\n\n a {\n margin: 0px 20px;\n\n @media (max-width: 767px) {\n font-size: 14px;\n padding: 12px 25px;\n margin: 0px 15px 15px 0px;\n }\n }\n }\n }\n }\n }\n\n /* video modal */\n .video-modal {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n z-index: 999;\n opacity: 0;\n filter:alpha(opacity=0);\n visibility: hidden;\n\n @include transition(all 0.25s linear);\n\n &.active {\n opacity: 1;\n filter:alpha(opacity=100);\n visibility: visible;\n background: rgba(44, 44, 45, 0.8);\n\n .wrap {\n @include transform(scale3d(1, 1, 1));\n opacity: 1;\n filter:alpha(opacity=100);\n }\n }\n\n .wrap {\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n margin: auto;\n padding: 5px;\n background: rgba(255, 255, 255, 0.65);\n border-radius: 5px;\n width: 630px;\n height: 360px;\n opacity: 0;\n filter:alpha(opacity=0);\n\n @include transform(scale3d(.3, .3, .3));\n @include transition(all 0.4s ease-out);\n }\n }\n}\n\n.features-hover-section {\n margin-top: 120px;\n\n @media(max-width: 991px) {\n margin-top: 60px;\n }\n\n .images {\n height: 355px;\n\n @media(max-width: 767px) {\n height: 300px;\n }\n\n img {\n margin: 0 auto;\n position: absolute;\n left: 0;\n right: 0;\n display: none;\n\n &.active {\n display: block;\n }\n }\n }\n\n .features {\n margin-top: 50px;\n\n @media(max-width: 991px) {\n margin-top: 0px;\n }\n\n .feature {\n cursor: pointer;\n \n @media(min-width: 992px) {\n width: 85%;\n margin: 0 auto;\n }\n\n &.active,\n &:hover {\n strong {\n color: #58B0D5;\n }\n\n p {\n color: #555;\n }\n }\n\n strong {\n color: #b6b6b6;\n font-size: 16px;\n\n @include transition(color 0.2s linear);\n }\n\n p {\n margin-top: 15px;\n line-height: 25px;\n color: #b6b6b6;\n\n @include transition(color 0.2s linear);\n }\n }\n }\n}\n",".sidebar-toggle {\n width: 30px;\n float: right;\n margin-top: 15px;\n cursor: pointer;\n \n .line {\n background: #fff;\n width: 100%;\n height: 3px;\n margin-bottom: 6px;\n border-radius: 1px;\n }\n}\n\n.st-container {\n position: relative;\n overflow: hidden;\n\n &--right-side {\n .nav-menu {\n right: 0;\n @include transform(translate3d(50%, 0, 0));\n }\n\n /* show hidden navbar */\n &.nav-effect {\n .nav-menu {\n @include transform(translate3d(0, 0, 0));\n }\n\n .st-pusher {\n @include transform(translate3d(-320px, 0, 0));\n box-shadow: 5px 0px 20px 0px rgba(0, 0, 0, 0.5);\n\n @media(max-width: 767px) {\n @include transform(translate3d(-220px, 0, 0));\n }\n }\n }\n }\n\n &--left-side {\n .nav-menu {\n left: 0;\n @include transform(translate3d(-50%, 0, 0));\n }\n\n /* show hidden navbar */\n &.nav-effect {\n .nav-menu {\n @include transform(translate3d(0, 0, 0));\n }\n\n .st-pusher {\n @include transform(translate3d(300px, 0, 0));\n box-shadow: -5px 0px 20px 0px rgba(0, 0, 0, 0.5);\n\n @media(max-width: 767px) {\n @include transform(translate3d(220px, 0, 0));\n }\n }\n }\n }\n}\n\n.st-pusher {\n position: relative;\n left: 0;\n z-index: 99;\n background: #fff;\n\n @include transitionPrefix(transform, 0.35s ease-out);\n}\n\n.nav-menu {\n position: fixed;\n top: 0;\n z-index: 1;\n width: 320px;\n height: 100%;\n background: #2a313a;\n padding-top: 30px;\n\n @include transitionPrefix(transform, 0.35s ease-out);\n\n @media(max-width: 767px) {\n width: 220px;\n position: absolute;\n }\n\n .main-menu {\n h3 {\n color: #fff;\n font-size: 26px;\n margin-left: 50px;\n margin-bottom: 30px;\n\n @media(max-width: 767px) {\n margin-left: 30px;\n font-size: 23px;\n }\n }\n\n a {\n display: block;\n padding: 5px 30px 5px 50px;\n margin-bottom: 10px;\n text-decoration: none;\n color: #fff;\n font-size: 15px;\n\n @include transition(all 0.15s linear);\n\n @media(max-width: 767px) {\n padding-left: 30px;\n font-size: 16px;\n }\n\n &:hover {\n color: #82C4F8;\n }\n }\n\n .social {\n margin-left: 45px;\n margin-top: 50px;\n\n @media(max-width: 767px) {\n margin-left: 30px;\n }\n\n a {\n padding: 0px;\n display: inline-block;\n margin-right: 22px;\n &:hover {\n i {\n color: #7EBDE7;\n }\n }\n\n i {\n font-size: 22px;\n color: #fff;\n\n @include transition(all 0.15s linear);\n }\n }\n }\n }\n\n .projects {\n .back {\n margin-top: 10px;\n font-weight: 500;\n font-size: 21px;\n color: #fff;\n margin-bottom: 20px;\n }\n\n a {\n display: block;\n position: relative;\n left: 0px;\n padding: 15px 40px;\n text-decoration: none;\n color: #fff;\n font-size: 16px;\n font-weight: 400;\n\n @include transition(all 0.15s linear);\n\n @media(max-width: 767px) {\n padding-left: 20px;\n }\n\n &:hover {\n left: 10px;\n }\n\n p {\n color: #A2B1C2;\n font-size: 14px;\n margin-top: 5px;\n }\n }\n }\n}\n\n.index-sidebar-hero {\n background: #3D4046;\n height: 630px;\n position: relative;\n overflow: hidden;\n box-shadow: 0px 1px 3px -1px rgba(0, 0, 0, 0.35);\n padding-top: 130px;\n\n @media (max-width: 991px) {\n padding-top: 100px;\n height: 550px;\n }\n\n #cover-image {\n position: absolute;\n top: 0;\n left: 0;\n height: 100%;\n width: 100%;\n padding-top: 105px;\n background-position: center center;\n background-size: cover;\n @include animationDelay(0.15s);\n background-image: url(\"../../images/bg4.png\");\n\n @media (max-width: 991px) {\n padding-top: 65px;\n }\n\n &:after {\n content: '';\n position: absolute;\n display: block;\n height: 100%;\n left: 0;\n width: 100%;\n top: 0;\n background: rgba(24, 22, 19, 0.4);\n }\n\n &:before {\n position: absolute;\n top: 10%;\n bottom: 0;\n left: 0;\n right: 0;\n content: '';\n background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, rgba(0,0,0,0)), color-stop(100%, rgba(0,0,0,0.25)));\n background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(0,0,0,0.25) 100%);\n background: -moz-linear-gradient(top, rgba(0,0,0,0),rgba(0,0,0,0.25));\n background: -o-linear-gradient(top, rgba(0,0,0,0),rgba(0,0,0,0.25));\n background: linear-gradient(top, rgba(0,0,0,0),rgba(0,0,0,0.25));\n }\n }\n\n .container {\n position: relative;\n z-index: 3;\n height: 100%;\n }\n\n .hero-text {\n text-align: center;\n font-size: 35px;\n font-weight: 400;\n color: #fff;\n margin-top: 50px;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.55);\n\n @include animationDelay(0.6s);\n\n @media (max-width: 991px) {\n font-size: 30px;\n }\n\n @media (max-width: 767px) {\n font-size: 27px;\n }\n }\n\n .cta {\n text-align: center;\n margin-top: 40px;\n \n @include animationDelay(0.6s);\n\n a {\n margin: 0px 20px;\n\n @media (max-width: 400px) {\n display: block;\n }\n\n &.button {\n border-radius: 50px;\n padding: 12px 36px;\n font-size: 18px;\n @include transition(all 0.3s linear); \n\n @media (max-width: 767px) {\n font-size: 16px;\n }\n\n @media (max-width: 400px) {\n width: 65%;\n margin: 0 auto;\n margin-bottom: 25px;\n }\n\n &:hover {\n box-shadow: rgba(0, 0, 0, 0.3) 0 1px 2px, inset rgba(255, 255, 255, 0.88) 0px 1px 3px -1px, rgba(0,0,0,0.3) 0 1px 2px, #69B1F3 0 1px 30px;\n }\n }\n\n &.demo {\n color: #fff;\n font-size: 17px;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.3);\n\n @media (max-width: 767px) {\n font-size: 16px;\n }\n }\n }\n }\n\n .screenshot {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n\n img {\n margin: 0 auto;\n }\n }\n}\n\n.cta-mobile-section {\n background: #F7F8FB;\n border: 1px solid #EAEDF7;\n box-shadow: inset rgba(100, 100, 100, 0.25) 0 1px 1px;\n margin-top: 100px;\n\n @media(min-width: 992px) {\n .container {\n width: 870px;\n }\n } \n\n .device {\n margin: -30px 0px 0px 0px;\n position: relative;\n top: 2px;\n }\n\n .info {\n margin-top: 50px;\n \n @media (max-width: 767px) {\n margin: 35px;\n text-align: center;\n }\n\n h3 {\n font-weight: 400;\n color: #616974;\n font-size: 26px;\n }\n\n p {\n font-size: 15px;\n margin-top: 20px;\n color: #6a6e7b;\n line-height: 23px;\n }\n\n .app-store {\n margin-top: 18px;\n border: none;\n display: inline-block;\n\n img {\n max-width: 135px;\n }\n }\n }\n}\n\n.call-to-action-signup {\n margin-top: 110px;\n margin-bottom: -120px;\n background: #fafafa;\n padding: 60px 0 70px;\n border-top: 1px solid #eee;\n border-bottom: 1px solid #eee;\n\n @media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n }\n\n h3 {\n margin: 0;\n font-size: 20px;\n letter-spacing: 2px;\n text-align: center;\n color: #4c4c4c;\n text-transform: uppercase;\n font-weight: 600;\n text-shadow: 1px 1px #fff;\n }\n\n form {\n margin-top: 40px;\n position: relative;\n text-align: center;\n\n .form-group {\n margin-right: 7px;\n width: 250px;\n\n @media(max-width: 991px) {\n width: 220px;\n }\n\n @media(max-width: 767px) {\n width: 100%;\n }\n }\n\n input {\n box-shadow: 0 1px 1px #fff;\n height: 42px;\n width: 100% !important;\n font-size: 15px;\n\n &:focus {\n box-shadow: none;\n }\n }\n\n [type=\"submit\"] {\n @media(max-width: 1199px) {\n margin-top: 30px;\n }\n }\n\n .agree {\n top: 20px;\n position: relative;\n display: block;\n text-align: right;\n padding-right: 95px;\n color: #555;\n\n @media(max-width: 1199px) {\n text-align: center;\n padding: 0;\n }\n }\n }\n}\n\n#demo {\n .modal-dialog {\n padding-top: 100px;\n width: 650px;\n }\n .modal-body {\n padding: 0px;\n }\n}\n",".features-tabs-section {\n margin-top: 60px;\n\n @media(max-width: 991px) {\n margin-bottom: 20px;\n }\n\n .header {\n text-align: center;\n\n h3 {\n font-size: 23px;\n color: #55565d;\n line-height: 25px;\n font-weight: 500;\n }\n\n p {\n font-size: 16px;\n font-weight: 400;\n color: #888;\n }\n }\n\n .tabs-wrapper {\n margin-top: 45px;\n\n .nav-tabs {\n text-align: center;\n border-bottom: 2px solid #EDEDED;\n \n @media(max-width: 991px) {\n display: none;\n }\n\n li {\n float: none;\n display: inline-block;\n\n &.active {\n a {\n color: #2B69A2;\n font-weight: 400;\n border-bottom: 0px;\n\n &:after {\n content: '';\n position: absolute;\n width: 100%;\n height: auto;\n border-bottom: 2px solid #7BA3C9;\n left: 1px;\n bottom: -1px;\n }\n }\n }\n\n a {\n color: #888;\n font-weight: 400;\n border: 0px;\n padding: 10px 20px;\n font-size: 15px;\n\n @include transition(color 0.25s linear);\n\n &:hover {\n color: #2B69A2;\n background: none;\n }\n }\n }\n }\n\n .tab-content {\n margin-top: 60px;\n\n .tab-pane {\n height: 325px;\n \n @media(max-width: 991px) {\n display: block !important;\n opacity: 1;\n filter: alpha(opacity=100);\n height: auto;\n text-align: center;\n margin-bottom: 70px;\n }\n\n .info {\n @media(max-width: 991px) {\n margin-bottom: 35px;\n }\n\n h4 {\n color: #3B88B6;\n font-weight: normal;\n font-size: 19px;\n line-height: 25px;\n margin-top: 35px;\n }\n\n p {\n color: #6C7279;\n font-size: 15px;\n margin-top: 20px;\n line-height: 24px;\n width: 83%;\n font-weight: normal;\n\n @media(max-width: 991px) {\n margin: 0 auto;\n width: 95%;\n }\n }\n }\n\n .image {\n img {\n display: inline-block;\n margin: 0 auto;\n }\n }\n }\n }\n } \n}\n\n.features-grid-section {\n margin-top: 100px;\n\n @media(max-width: 991px) {\n margin-top: 50px;\n }\n\n .row {\n @media(min-width: 992px) {\n margin-bottom: 50px;\n }\n }\n\n .feature {\n @media(max-width: 991px) {\n text-align: center;\n margin-bottom: 35px;\n }\n\n img {\n max-width: 40px;\n }\n\n .icon {\n font-size: 32px;\n color: #4187ba;\n }\n\n strong {\n display: block;\n font-weight: 400;\n font-size: 17px;\n color: #354E75;\n margin-top: 15px;\n }\n\n p {\n font-weight: normal;\n font-size: 14px;\n color: #797979;\n margin-top: 10px;\n line-height: 24px;\n }\n }\n}\n\n.blog-showcase {\n margin-top: 120px;\n\n .header {\n text-align: center;\n margin-bottom: 45px;\n\n h3 {\n font-size: 25px;\n color: #444;\n line-height: 25px;\n font-weight: 400;\n }\n }\n\n .pics {\n text-align: center;\n\n .pic {\n position: relative;\n width: 250px;\n height: 258px;\n margin: 0 auto;\n margin-left: 10px;\n margin-right: 10px;\n display: inline-block;\n opacity: 1;\n filter: alpha(opacity=100);\n\n @include transition(all 0.25s linear);\n \n @media(max-width: 991px) {\n margin-bottom: 35px;\n }\n\n &:hover {\n opacity: 0.7;\n filter: alpha(opacity=70);\n text-decoration: none;\n }\n\n .bg {\n position: absolute;\n padding: 80px 20px 20px;\n bottom: 0;\n left: 0;\n right: 0;\n border-radius: 0px 5px 5px 0px;\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(bottom, rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.3) 30%, rgba(0, 0, 0, 0) 100%);\n background-image: -moz-linear-gradient(bottom, rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.3) 30%, rgba(0, 0, 0, 0) 100%);\n background-image: -ms-linear-gradient(bottom, rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.3) 30%, rgba(0, 0, 0, 0) 100%);\n background-image: -o-linear-gradient(bottom, rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.3) 30%, rgba(0, 0, 0, 0) 100%);\n background-image: linear-gradient(to top, rgba(0, 0, 0, 0.12) 0, rgba(0, 0, 0, 0.3) 30%, rgba(0, 0, 0, 0) 100%);\n }\n\n img {\n display: block;\n margin: 0 auto;\n border-radius: 5px;\n }\n\n p {\n text-align: left;\n color: #fff;\n font-size: 22px;\n position: absolute;\n line-height: 28px;\n bottom: 7px;\n left: 20px;\n width: 75%;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.4);\n }\n }\n } \n}\n",".services-intro {\n margin-top: 90px;\n\n @media(min-width: 992px) {\n .row {\n border-bottom: 1px solid #E6E6E8;\n }\n }\n\n .image {\n img {\n @media(max-width: 991px) {\n margin: 0 auto;\n }\n }\n }\n\n .info { \n @media(min-width: 992px) {\n padding-left: 60px;\n }\n \n @media(max-width: 991px) {\n text-align: center\n }\n\n h3 {\n font-weight: 400;\n margin-top: 70px;\n font-size: 28px;\n }\n\n p {\n color: #666;\n margin-top: 25px;\n line-height: 23px;\n\n @media(min-width: 992px) {\n width: 85%;\n }\n }\n }\n}\n\n.services-tabs {\n margin-top: 130px;\n\n @media(max-width: 991px) {\n margin-top: 70px;\n }\n\n .hexagons {\n text-align: center;\n\n .hexagon {\n background: url(\"../../images/hexagon.png\") 0 0 no-repeat;\n width: 100px;\n height: 113px;\n position: relative;\n display: inline-block;\n margin-right: 60px;\n cursor: pointer;\n\n @media(max-width: 991px) {\n margin: 0;\n }\n\n &:hover,\n &.active {\n background-position: 0 -117px;\n .icon {\n color: #fff;\n }\n }\n\n &:last-child {\n margin-right: 0px;\n }\n\n .icon {\n font-size: 48px;\n color: #80939b;\n position: absolute;\n top: 32px;\n left: 0;\n right: 0;\n text-align: center;\n }\n }\n }\n\n .sections {\n margin-top: 35px;\n position: relative;\n height: 220px;\n\n .section {\n display: none;\n position: absolute;\n\n &.active {\n display: block;\n }\n\n h4 {\n margin-bottom: 18px;\n }\n\n p {\n line-height: 25px;\n }\n }\n }\n}\n\n.services-grid-section {\n margin-top: 100px;\n\n &.with-border {\n margin-top: 70px;\n\n .header {\n border-top: 1px solid #E6E6E8;\n padding-top: 55px;\n }\n }\n\n .header {\n text-align: center;\n\n h3 {\n font-size: 26px;\n color: #555C68;\n line-height: 25px;\n font-weight: 400;\n }\n\n p {\n font-size: 16px;\n font-weight: 400;\n color: #888;\n }\n }\n\n .sections {\n margin-top: 45px;\n\n .section {\n text-align: center;\n\n @media(max-width: 991px) {\n margin-bottom: 40px;\n }\n\n .pic {\n display: inline-block;\n padding: 15px;\n background: #f5f5f5;\n box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.25), inset 0px 0px 1px 1px #fff;\n }\n\n .info {\n margin-top: 25px;\n text-align: center;\n\n @media(min-width: 992px) {\n margin-left: 45px;\n width: 80%;\n text-align: left;\n }\n\n strong {\n font-weight: 500;\n font-size: 18px;\n color: #555;\n }\n\n p {\n color: #666;\n margin-top: 15px;\n line-height: 22px;\n }\n }\n }\n }\n}\n\n.services-showcase-section {\n margin-top: 80px;\n\n @media(max-width: 991px) {\n margin-top: 50px;\n }\n\n .header {\n border-top: 1px solid #E6E6E8;\n padding-top: 55px;\n margin-bottom: 35px;\n\n h3 {\n font-size: 27px;\n color: #444;\n line-height: 25px;\n font-weight: 500;\n margin-bottom: 7px;\n\n @media(max-width: 991px) {\n text-align: center;\n }\n }\n\n p {\n font-size: 15px;\n font-weight: 400;\n color: #666;\n width: 50%;\n line-height: 24px;\n margin-top: 15px;\n\n @media(max-width: 991px) {\n margin: 0 auto;\n width: 90%;\n text-align: center;\n }\n }\n }\n\n .features {\n margin-bottom: 20px;\n\n .feature { \n @media(max-width: 991px) {\n text-align: center;\n margin-bottom: 30px;\n }\n\n img {\n max-width: 40px;\n position: relative;\n top: 10px;\n\n @media(min-width: 992px) {\n float: left;\n }\n }\n\n .info {\n @media(min-width: 992px) {\n float: left;\n width: 80%;\n margin-left: 20px;\n }\n\n strong {\n display: block;\n font-weight: 500;\n font-size: 16px;\n color: #444;\n margin-top: 15px;\n }\n\n p {\n font-weight: normal;\n font-size: 14px;\n color: #797979;\n margin-top: 10px;\n line-height: 24px;\n }\n } \n }\n }\n}\n\n.signup-cta {\n margin-top: 120px;\n\n .wrapper {\n background: #F7F8FB;\n border: 1px solid #EAEDF7;\n border-radius: 8px;\n padding: 30px 60px;\n box-shadow: inset rgba(100, 100, 100, 0.25) 0 1px 1px; \n margin: 0 auto;\n\n @media(max-width: 991px) {\n text-align: center;\n }\n }\n\n h4 {\n font-size: 20px;\n color: #5F6E7C;\n font-weight: 400;\n float: left;\n text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.8);\n\n @media(max-width: 767px) {\n font-size: 17px;\n line-height: 23px;\n }\n }\n\n .button {\n float: right;\n\n @media(max-width: 991px) {\n float: none;\n margin-top: 20px;\n }\n }\n}\n",".session-page {\n min-height: 100vh;\n background-attachment: fixed !important;\n background: #5E7A9B;\n background: -webkit-linear-gradient(top, #355069 0, #5E7A9B 100%);\n background: -moz-linear-gradient(top, #355069 0, #5E7A9B 100%);\n background: -o-linear-gradient(top, #355069 0, #5E7A9B 100%);\n background: linear-gradient(top, #355069 0, #5E7A9B 100%);\n\n .header {\n .logo {\n margin-top: 40px;\n text-align: center;\n \n a {\n color: #fff;\n font-size: 31px;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.6);\n \n @include transition(color .25s linear);\n \n &:hover {\n text-decoration: none;\n color: #bbb;\n }\n }\n }\n\n h4 {\n color: #fff;\n text-align: center;\n font-size: 25px;\n font-weight: 400;\n margin-top: 45px;\n margin-bottom: 5px;\n }\n\n p {\n color: #fff;\n text-align: center;\n }\n }\n \n .wrapper {\n margin: 0 auto;\n width: 51%;\n margin-top: 25px;\n background: #fff;\n padding: 15px 50px 25px 50px;\n border-radius: 5px;\n border-top: 6px solid #6ED5E4;\n\n @media(max-width: 991px) {\n width: 67%;\n }\n\n @media(max-width: 767px) {\n width: 100%;\n }\n\n .formy {\n margin-top: 22px;\n\n .form-group {\n label {\n color: #46515F;\n }\n }\n\n .checkbox {\n label {\n color: #666;\n font-size: 13px;\n }\n\n input {\n position: relative;\n top: -1px;\n }\n }\n\n .submit {\n text-align: center;\n\n .button-clear {\n margin-top: 35px;\n display: inline-block;\n }\n }\n\n .info {\n float: right;\n width: 67%;\n\n .good-company {\n strong {\n font-size: 15px;\n font-weight: 500;\n }\n\n p {\n color: #858585;\n margin-top: 9px;\n }\n }\n\n .testimonial {\n margin-top: 40px;\n\n .quote {\n font-style: italic;\n font-size: 13px;\n color: #555;\n line-height: 20px;\n }\n\n .author {\n margin-top: 20px;\n\n .pic {\n width: 55px;\n height: 55px;\n border-radius: 50px;\n float: left;\n position: relative;\n top: -6px;\n margin-right: 18px;\n }\n\n .name {\n color: #3C92C5;\n line-height: 23px;\n font-weight: 500;\n }\n\n .company {\n font-size: 14px;\n color: #909090;\n line-height: 23px;\n font-weight: 400;\n }\n }\n }\n }\n }\n }\n\n .already-account {\n text-align: center;\n color: #fff;\n margin-top: 25px;\n margin-bottom: 70px;\n font-size: 13px;\n font-weight: 500;\n\n a {\n color: #fff;\n text-decoration: underline;\n }\n\n .popover-content {\n color: #000;\n font-size: 17px;\n font-weight: 400;\n }\n }\n}\n",".signup-rotate {\n min-height: 100vh;\n background-attachment: fixed !important;\n \n background: #5E7A9B;\n background: -webkit-linear-gradient(top, #355069 0, #5E7A9B 100%);\n background: -moz-linear-gradient(top, #355069 0, #5E7A9B 100%);\n background: -o-linear-gradient(top, #355069 0, #5E7A9B 100%);\n background: linear-gradient(top, #355069 0, #5E7A9B 100%);\n\n .header {\n .logo {\n margin-top: 40px;\n text-align: center;\n\n a {\n color: #fff;\n font-size: 31px;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.6);\n\n @include transition(color .25s linear);\n\n &:hover {\n text-decoration: none;\n color: #bbb;\n }\n }\n }\n\n h4 {\n color: #fff;\n text-align: center;\n font-size: 25px;\n font-weight: 400;\n margin-top: 45px;\n margin-bottom: 5px;\n }\n\n p {\n color: #fff;\n text-align: center;\n }\n }\n \n .rotate-container {\n -webkit-perspective: 1000;\n -moz-perspective: 1000;\n -ms-perspective: 1000;\n -o-perspective: 1000;\n margin: 0 auto;\n }\n\n .wrappers {\n min-height: 445px;\n\n @include transitionPrefix(transform, 1s);\n \n -webkit-transform-style: preserve-3d;\n -moz-transform-style: preserve-3d;\n -ms-transform-style: preserve-3d;\n -o-transform-style: preserve-3d;\n transform-style: preserve-3d;\n\n &.flipped {\n @include transform(rotateY(180deg));\n }\n\n &.flipped-ended {\n .front {\n z-index: 1;\n }\n .back {\n z-index: 3;\n }\n }\n }\n\n .wrapper {\n margin: 0 auto;\n width: 51%;\n margin-top: 25px;\n background: #fff;\n padding: 15px 50px 25px 50px;\n border-radius: 5px;\n border-top: 6px solid #6ED5E4;\n\n @media(max-width: 991px) {\n width: 67%;\n }\n\n @media(max-width: 767px) {\n width: 100%;\n }\n\n position: absolute;\n left: 0;\n right: 0;\n\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n -o-backface-visibility: hidden;\n\n /* These are commented out because they break the rotation on IE */\n /*-ms-backface-visibility: hidden;*/\n /*backface-visibility: hidden;*/\n\n -webkit-transform-style: preserve-3d;\n -moz-transform-style: preserve-3d;\n -ms-transform-style: preserve-3d;\n -o-transform-style: preserve-3d;\n transform-style: preserve-3d;\n\n &.front {\n z-index: 3;\n }\n\n &.back {\n z-index: 1;\n\n @include transform(rotateY(180deg));\n\n h3 {\n margin-top: -5px;\n margin-bottom: 45px;\n text-align: center;\n font-weight: 400;\n }\n }\n\n .formy {\n margin-top: 22px;\n\n .form-group {\n label {\n color: #46515F;\n }\n }\n\n .checkbox {\n label {\n color: #666;\n font-size: 13px;\n }\n\n input {\n position: relative;\n top: -1px;\n }\n }\n\n .submit {\n text-align: center;\n\n .button-clear {\n margin-top: 35px;\n display: inline-block;\n }\n }\n }\n }\n\n .change-view {\n text-align: center;\n color: #fff;\n margin-top: 25px;\n margin-bottom: 70px;\n font-size: 13px;\n font-weight: 500;\n display: none;\n\n &.active {\n display: block;\n }\n\n a {\n color: #fff;\n text-decoration: underline;\n }\n\n .popover-content {\n color: #000;\n font-size: 17px;\n font-weight: 400;\n }\n }\n}\n",".blog-posts {\n margin-top: 60px;\n\n .post {\n margin-bottom: 90px;\n\n .pic {\n border: 0px;\n\n &:hover {\n img {\n opacity: 0.9;\n filter: alpha(opacity=90);\n }\n }\n\n img {\n opacity: 1;\n filter: alpha(opacity=100);\n @include transition(all 0.25s linear);\n\n @media(max-width: 991px) {\n margin: 0 auto;\n }\n }\n }\n\n .video {\n @media(min-width: 992px) {\n max-width: 635px;\n }\n\n /* make video fluid for responsiveness */\n .iframe-wrapper {\n position: relative;\n padding-bottom: 56%;\n height: 0;\n\n iframe {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n }\n }\n }\n\n .title {\n font-family: $myriad;\n font-size: 28px;\n font-weight: 600;\n margin-top: 33px;\n\n @media(max-width: 991px) {\n text-align: center;\n margin-bottom: 20px;\n }\n\n @media(max-width: 767px) {\n font-size: 23px;\n }\n\n a {\n color: #111;\n\n @include transition(color 0.15s linear);\n\n &:hover {\n text-decoration: none;\n color: #428bca;\n }\n }\n }\n\n .author {\n color: #888;\n margin-top: 4px;\n position: relative;\n\n .avatar {\n max-width: 60px;\n border-radius: 50px;\n position: absolute;\n left: -75px;\n top: -15px;\n border: 1px solid #e0e0e0;\n }\n }\n\n .intro {\n font-family: $sourceSans;\n margin-top: 18px;\n font-size: 16px;\n line-height: 27px;\n color: #4d4d4d;\n \n @media(min-width: 992px) {\n width: 90%;\n }\n }\n\n .continue-reading {\n font-family: $sourceSans;\n font-size: 15px;\n display: inline-block;\n margin-top: 8px;\n @include transition(color 0.25s linear);\n }\n }\n\n .pages {\n @media(max-width: 991px) {\n text-align: center;\n }\n }\n\n .sidebar {\n @media(max-width: 991px) {\n margin-top: 70px;\n text-align: center;\n }\n\n .search {\n margin-bottom: 40px;\n\n form {\n position: relative;\n left: -20px;\n }\n\n .icomoon-search {\n font-size: 16px;\n color: #B9B9B9;\n position: relative;\n left: 25px;\n top: 1px;\n\n @include transition(all 0.25s linear);\n\n &.active {\n color: #555;\n }\n }\n\n input {\n border: 0px;\n border-bottom: 1px solid #DFDFDF;\n padding-left: 30px;\n padding-bottom: 6px;\n @include transition(all 0.25s linear);\n color: #777;\n font-weight: 300;\n\n @media(max-width: 991px) {\n width: 95%;\n }\n \n &:focus {\n border-bottom-color: #888;\n }\n\n @include placeholder {\n color: #a5a5a5;\n font-weight: 300;\n font-size: 13px;\n }\n }\n }\n\n .updates {\n padding: 17px;\n box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.2);\n border-radius: 5px;\n\n @media(min-width: 992px) {\n width: 85%;\n }\n\n strong {\n color: #444;\n }\n\n .fa-rss {\n color: #428bca;\n font-size: 16px;\n position: relative;\n margin-left: 5px;\n }\n\n p {\n font-size: 13px;\n margin-top: 9px;\n color: #777;\n margin-bottom: 5px;\n }\n }\n\n .follow-tw {\n margin-top: 20px;\n\n img {\n cursor: pointer;\n }\n }\n\n .best-hits {\n margin-top: 35px;\n padding-left: 5px;\n\n @media(max-width: 991px) {\n margin-top: 40px;\n }\n\n strong {\n font-weight: 500;\n color: #444;\n margin-bottom: 15px;\n font-size: 15px;\n display: block;\n }\n\n a {\n font-size: 13px;\n display: inline-block;\n margin-bottom: 5px;\n\n @media(max-width: 991px) {\n display: block;\n margin-bottom: 10px;\n }\n }\n }\n }\n}\n",".blog-cols-page {\n background: #FCFCFC;\n}\n\n.blog-cols-header {\n margin-top: 60px;\n position: relative;\n\n h1 {\n font-size: 23px;\n margin-top: 0;\n color: #555;\n }\n\n p {\n color: #757575;\n font-size: 17px;\n margin-top: 12px;\n }\n}\n\n.blog-cols-wrapper {\n margin-top: 40px;\n\n @media(max-width: 767px) {\n text-align: center;\n }\n\n .card {\n display: inline-block;\n\n @media(min-width: 992px) {\n &:nth-child(3n+3) {\n .card-wrapper {\n margin: 20px 0;\n }\n }\n }\n\n @media(min-width: 768px) and (max-width: 991px) {\n &:nth-child(2n+2) {\n .card-wrapper {\n margin: 20px 0;\n }\n }\n }\n\n .card-wrapper {\n display: inline-block;\n position: relative;\n width: 295px;\n background-color: #fff;\n vertical-align: top;\n text-align: left;\n color: #4B4F56;\n height: 480px;\n margin: 20px 20px 20px 0;\n box-shadow: 0 20px 20px rgba(0,0,0,.08);\n\n @include transition(all 250ms cubic-bezier(.02, .01, .47, 1));\n\n @media(max-width: 991px) {\n width: 340px;\n }\n\n @media(max-width: 767px) {\n margin: 20px 0;\n width: 290px;\n }\n\n &:hover {\n box-shadow: 0 40px 40px rgba(0,0,0,.16);\n transform: translate(0,-20px);\n }\n }\n\n .card-bg {\n background-size: cover;\n background-position: center center;\n background-repeat: no-repeat;\n width: 100%;\n height: 295px;\n\n @media(max-width: 1199px) {\n height: 260px;\n }\n }\n\n .card-intro {\n padding: 20px 20px 0;\n }\n\n .card-title {\n font-size: 22px;\n line-height: 32px;\n }\n\n .card-min-read {\n margin-top: 5px;\n font-size: 12px;\n color: #777;\n }\n\n .card-description {\n font-size: 15px;\n margin-top: 13px;\n line-height: 23px;\n color: #90949C;\n }\n }\n}\n\n.blog-pager {\n margin-top: 80px;\n\n li>a {\n padding: 9px 20px;\n box-shadow: 0 1px 0px 0 rgba(0, 0, 0, 0.15);\n border-radius: 30px;\n margin: 0 10px;\n font-weight: 500;\n font-size: 15px;\n color: #5F8AB0;\n display: inline-block;\n\n @include transition(all .2s linear);\n\n &:hover {\n background-color: #4a556b;\n color: #fff;\n box-shadow: none;\n }\n }\n}\n",".timeline-page {\n background: #f7f8fa;\n}\n\n.blog-timeline-header {\n margin-top: 55px;\n position: relative;\n\n h1 {\n text-align: center;\n text-transform: uppercase;\n font-weight: 500;\n font-size: 24px;\n margin-top: 0;\n letter-spacing: 1px;\n\n a {\n color: #222;\n text-decoration: none;\n }\n }\n\n .slogan {\n text-align: center;\n color: #777;\n font-size: 16px;\n }\n}\n\n.blog-timeline-wrapper {\n margin-top: 40px;\n position: relative;\n\n &:before {\n content: '';\n position: absolute;\n background: #e7e8ec;\n width: 5px;\n height: 96%;\n left: 0;\n right: 0;\n top: 95px;\n margin: 0 auto;\n z-index: -1;\n }\n\n .post {\n position: relative;\n float: right;\n clear: right;\n width: 44%;\n margin: 40px 0;\n border-radius: 5px;\n\n @media(max-width: 767px) {\n width: 100%;\n }\n\n &:before {\n content: '';\n position: absolute;\n width: 70px;\n height: 4px;\n background: #E7E8EC;\n left: -60px;\n top: 62px;\n\n @media(max-width: 991px) {\n left: -42px;\n width: 43px;\n }\n\n @media(max-width: 767px) {\n display: none;\n }\n }\n\n &:nth-child(2) {\n margin-top: 100px;\n }\n\n &.left {\n float: left;\n clear: left;\n\n &:before {\n right: -51px;\n left: inherit;\n\n @media(max-width: 991px) {\n right: -42px;\n width: 43px;\n }\n }\n\n .marker {\n left: inherit;\n right: -65px;\n\n @media(max-width: 991px) {\n right: -52px;\n }\n }\n }\n\n .marker {\n position: absolute;\n width: 17px;\n height: 17px;\n border-radius: 25px;\n background: #e7e8ec;\n left: -65px;\n top: 55px;\n border: 3px solid #F7F8FA;\n\n @media(max-width: 991px) {\n left: -51px;\n }\n\n @media(max-width: 767px) {\n display: none;\n }\n }\n\n .entry {\n position: relative;\n padding: 20px;\n height: 400px;\n background-size: cover;\n background-position: center center;\n border-radius: 5px;\n box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.26);\n\n &:before {\n position: absolute;\n top: 50%;\n bottom: 0;\n left: 0;\n right: 0;\n content: '';\n border-radius: 0 0 5px 5px;\n background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.94) 100%);\n background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.94) 100%);\n background: -o-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.94) 100%);\n background: linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.94) 100%);\n }\n\n &.smallish {\n height: 320px;\n\n .intro {\n top: 68%;\n }\n }\n\n .intro {\n position: relative;\n top: 75%;\n color: #FFF;\n z-index: 9;\n letter-spacing: .3px;\n\n h4 {\n font-size: 19px;\n font-weight: 600;\n text-transform: uppercase;\n text-shadow: 1px 1px rgba(0, 0, 0, 0.4);\n letter-spacing: .5px;\n\n @media(max-width: 767px) {\n font-size: 17px;\n }\n }\n\n p {\n @media(max-width: 767px) {\n font-size: 13px;\n }\n }\n }\n }\n\n .news {\n position: relative;\n background-size: cover;\n background-position: center center;\n border-radius: 5px;\n box-shadow: 0 1px 5px rgba(12, 26, 36, 0.25);\n background: #fff;\n\n .author {\n position: absolute;\n left: 0;\n top: -35px;\n right: 0;\n text-align: center;\n\n img {\n border-radius: 50%;\n width: 68px;\n }\n }\n\n section {\n padding: 60px 25px 75px 25px;\n\n h3 {\n margin-top: 0;\n\n a {\n color: #333;\n text-decoration: none;\n font-size: 21px;\n text-align: center;\n display: block;\n line-height: 31px;\n\n @include transition(all .15s);\n\n &:hover {\n color: #459CE7;\n }\n }\n }\n\n p {\n margin-top: 21px;\n line-height: 24px;\n color: #555;\n }\n\n a.read-more {\n position: relative;\n text-decoration: none;\n display: inline-block;\n margin-top: 10px;\n font-size: 15px;\n\n @include transition(all .2s);\n\n &:hover {\n &:after {\n right: -18px;\n }\n }\n\n &:after {\n content: \"▸\";\n position: absolute;\n right: -15px;\n top: 0;\n\n @include transition(all .2s);\n }\n }\n }\n\n footer {\n position: absolute;\n bottom: 0;\n padding: 12px 25px;\n width: 100%;\n font-weight: 500;\n background: #EEF1F4;\n color: #92A2B2;\n\n .tag {\n position: relative;\n padding-left: 27px;\n text-transform: uppercase;\n font-weight: 600;\n letter-spacing: 1px;\n font-size: 11px;\n top: 3px;\n\n span {\n font-size: 24px;\n position: absolute;\n top: -7px;\n left: 0;\n }\n }\n\n .date {\n font-weight: 600;\n font-size: 14px;\n }\n }\n }\n }\n\n .pager {\n text-align: center;\n margin-top: 50px;\n margin-bottom: 0;\n\n a {\n display: inline-block;\n position: relative;\n top: -10px;\n background: #E7E8EC;\n text-decoration: none;\n text-transform: uppercase;\n font-weight: 600;\n letter-spacing: 1px;\n padding: 6px 23px;\n border-radius: 3px;\n color: #444;\n\n @include transition(all .2s);\n\n &:hover {\n box-shadow: 0 6px 15px -1px rgba(0, 0, 0, 0.15);\n }\n }\n }\n}\n",".blog-post-hero {\n height: 500px;\n background-image: url('../../images/photo-1470753937643-efeb931202a9.jpeg');\n background-size: cover;\n background-position: center center;\n position: relative;\n top: -20px;\n\n @media(max-width: 991px) {\n height: 400px;\n }\n\n &:after {\n content: '';\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n background: rgba(64, 64, 64, 0.5);\n }\n\n .container {\n position: relative;\n z-index: 1;\n text-align: center;\n }\n\n h1 {\n width: 60%;\n margin: 190px auto 0;\n color: #fff;\n font-family: $karla;\n font-size: 38px;\n line-height: 46px;\n text-shadow: 1px 1px rgba(0, 0, 0, 0.2);\n\n @media(max-width: 991px) {\n margin-top: 150px;\n font-size: 35px;\n line-height: 40px;\n width: 70%;\n }\n\n @media(max-width: 767px) {\n width: 100%;\n font-size: 28px;\n line-height: 35px;\n }\n }\n}\n\n.blog-post-author {\n color: #888;\n display: inline-block;\n\n img {\n border-radius: 100%;\n width: 55px;\n margin-right: 17px;\n position: relative;\n top: -2px;\n }\n\n span {\n color: #333;\n }\n}\n\n.blog-post-date {\n display: inline-block;\n margin-left: 35px;\n color: #888;\n\n @media(max-width: 767px) {\n margin-left: 15px;\n }\n\n span {\n color: #333;\n }\n}\n\n.blog-post-share {\n float: right;\n color: #888;\n\n @media(max-width: 767px) {\n float: none;\n }\n\n a {\n margin-left: 13px;\n position: relative;\n top: 3px;\n text-decoration: none;\n }\n\n i {\n font-size: 25px;\n\n &.ion-social-twitter {\n color: #1da1f2;\n }\n\n &.ion-social-facebook {\n color: #3b5998;\n font-size: 28px;\n margin-right: 3px;\n }\n\n &.ion-social-buffer {\n color: #2d2d2d;\n }\n }\n}\n\n.blog-post-wrapper {\n margin-top: 30px;\n\n .post {\n float: none;\n margin: 0 auto;\n margin-bottom: 50px;\n\n .title {\n font-size: 31px;\n font-weight: 600;\n margin-top: 40px;\n color: #252525;\n\n @media(max-width: 767px) {\n font-size: 26px;\n line-height: 28px;\n }\n }\n\n .content {\n margin-top: 45px;\n font-size: 15px;\n line-height: 30px;\n color: #454545;\n margin-bottom: 25px;\n\n p {\n margin-top: 28px;\n }\n\n blockquote {\n border-left: 2px solid #c0c9dc;\n color: rgba(51, 51, 51, 0.55);\n margin: 35px 0;\n padding: 5px 20px;\n padding-left: 23px;\n\n p {\n margin: 0;\n line-height: 30px;\n font-weight: normal;\n }\n\n .quote-author {\n margin-top: 14px;\n }\n }\n \n img {\n margin: 0 auto;\n margin-top: 55px;\n margin-bottom: 55px;\n }\n\n .divider {\n border-bottom: 1px solid #DEDEDC;\n width: 20%;\n margin: 0 auto;\n margin-top: 35px;\n margin-bottom: 35px;\n }\n }\n\n .share {\n margin-top: 50px;\n }\n\n .other-posts {\n margin-top: 50px;\n }\n } \n}\n",".pricing-3-options-section {\n margin-top: 70px;\n\n .header {\n text-align: center;\n\n h3 {\n font-size: 24px;\n color: #444;\n line-height: 25px;\n font-weight: 500;\n }\n\n p {\n font-size: 16px;\n font-weight: 400;\n color: #888;\n }\n }\n\n .charts {\n margin-top: 45px;\n\n .chart {\n background-color: #fff;\n border: 1px solid #D8D8D8;\n border-radius: 5px; \n box-shadow: 0px 0px 2px 0px rgba(181, 181, 181, 0.3);\n padding: 20px 40px;\n position: relative;\n text-align: center;\n width: 97%;\n min-height: 315px;\n position: relative;\n top: 20px;\n\n @media(min-width: 992px) {\n &.first {\n float: right;\n left: 45px;\n }\n\n &.last {\n left: -45px;\n }\n }\n\n @media (max-width: 991px) {\n float: none !important;\n left: 0px !important;\n top: 0px !important;\n margin: 0 auto;\n width: 55%;\n margin-bottom: 25px !important;\n\n &.last {\n margin-bottom: 0px !important;\n }\n }\n\n @media (max-width: 600px) {\n width: 90% !important;\n } \n\n &.featured { \n z-index: 999;\n\n @media(min-width: 992px) {\n margin: 0 auto;\n top: 0px;\n min-height: 445px;\n\n .btn-signup {\n margin-top: 32px;\n }\n }\n\n .popular {\n position: absolute;\n top: 0px;\n left: 0px;\n }\n\n .quantity {\n margin-top: 15px;\n padding-bottom: 5px;\n }\n }\n\n .plan-name {\n text-align: center;\n font-size: 20px;\n font-weight: 400;\n color: #444;\n width: 90%;\n margin: 0 auto;\n margin-top: 5px;\n }\n\n .quantity {\n text-align: center;\n border-bottom: 1px solid #d5d5d5;\n margin-top: 10px;\n\n .dollar {\n font-size: 19px;\n position: relative;\n top: -18px;\n }\n\n .price {\n font-size: 49px;\n }\n\n .period {\n font-size: 17px;\n position: relative;\n top: -8px;\n margin-left: 4px;\n }\n }\n\n .specs {\n margin-top: 20px;\n\n .spec {\n font-size: 15px;\n color: #474747;\n text-align: center;\n font-weight: 300;\n margin-bottom: 13px;\n\n .variable {\n color: #1FBEE7;\n font-weight: 500;\n }\n }\n }\n\n .btn-signup {\n margin-top: 20px;\n height: 38px;\n }\n } \n }\n\n .message {\n p {\n text-align: center;\n font-size: 13px;\n color: #656565;\n margin-top: 23px;\n }\n }\n\n .faq {\n margin-top: 40px;\n\n .header h3 {\n font-size: 20px;\n color: #454545;\n text-align: left;\n }\n\n .questions {\n margin-top: 25px;\n\n .question {\n margin-bottom: 40px;\n \n @media(min-width: 992px) {\n width: 90%;\n }\n\n strong {\n font-weight: 500;\n font-size: 16px;\n color: #454545;\n }\n\n p {\n color: #555;\n margin-top: 10px;\n font-size: 13px;\n line-height: 22px;\n }\n }\n }\n }\n\n .contact {\n margin-top: 40px;\n\n .wrapper {\n background: #F7F8FB;\n border: 1px solid #EAEDF7;\n border-radius: 8px;\n padding: 25px 60px;\n box-shadow: inset rgba(100, 100, 100, 0.25) 0 1px 1px; \n margin: 0 auto;\n }\n\n h4 {\n font-size: 15px;\n position: relative;\n color: #696E72;\n text-align: center;\n font-weight: 500;\n text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.8);\n \n @media (max-width: 991px) {\n line-height: 26px;\n }\n\n span {\n position: absolute;\n font-size: 23px;\n top: -5px;\n left: 72px;\n color: #626E7A;\n\n @media (max-width: 991px) {\n position: relative;\n left: 0px;\n top: 5px;\n }\n }\n }\n }\n}\n\n.pricing-dark-section {\n border-top: 1px solid #EAEAEA;\n border-bottom: 1px solid #EAEAEA;\n background: #5E7A9B;\n background: -webkit-linear-gradient(top, #355069 0, #5E7A9B 100%);\n background: -moz-linear-gradient(top, #355069 0, #5E7A9B 100%);\n background: -o-linear-gradient(top, #355069 0, #5E7A9B 100%);\n background: linear-gradient(top, #355069 0, #5E7A9B 100%);\n margin-top: 120px;\n padding-top: 45px;\n padding-bottom: 70px;\n \n .header {\n text-align: center;\n\n @media (max-width: 991px) {\n margin-bottom: 25px;\n }\n\n h3 {\n font-size: 27px;\n color: #fff;\n line-height: 25px;\n font-weight: 400;\n text-shadow: 0 1px 2px rgba(0,0,0,0.2);\n\n @media(max-width: 767px) {\n font-size: 24px;\n line-height: 29px;\n }\n }\n\n p {\n font-size: 16px;\n margin-top: 15px;\n color: #F0F0F0;\n font-weight: 400;\n line-height: 25px;\n text-shadow: 0 1px 2px rgba(0,0,0,0.2);\n }\n }\n\n .charts {\n margin-top: 35px;\n \n @media (max-width: 991px) {\n margin-top: 10px;\n }\n\n .chart {\n background-color: #fff;\n border: 1px solid #E6E6E6;\n border-radius: 5px; \n box-shadow: 0px 0px 2px 0px rgba(181, 181, 181, 0.3);\n padding: 20px 40px;\n position: relative;\n text-align: center;\n width: 97%;\n\n min-height: 315px;\n position: relative;\n top: 37px;\n\n @media (max-width: 991px) {\n float: none !important;\n left: 0px !important;\n top: 0px !important;\n margin: 0 auto;\n width: 55%;\n margin-bottom: 25px !important;\n\n &.last {\n margin-bottom: 0px !important;\n }\n }\n\n @media (max-width: 600px) {\n width: 90% !important;\n }\n\n &.first {\n float: right;\n left: 12px;\n }\n\n &.last {\n left: -12px;\n }\n\n &.featured {\n min-height: 350px;\n top: 0px;\n margin: 0 auto;\n\n .popular {\n text-align: center;\n text-transform: uppercase;\n font-size: 16px;\n letter-spacing: 1px;\n color: #B1B1B1;\n font-weight: 400;\n }\n\n .quantity {\n margin-top: 15px;\n }\n } \n\n .quantity {\n text-align: center;\n\n .dollar {\n font-size: 19px;\n position: relative;\n top: -18px;\n }\n\n .price {\n font-size: 49px;\n }\n\n .period {\n font-size: 17px;\n position: relative;\n top: -8px;\n margin-left: 4px;\n }\n }\n\n .plan-name {\n text-align: center;\n font-size: 20px;\n font-weight: 400;\n color: #777;\n border-bottom: 1px solid #d5d5d5;\n padding-bottom: 15px;\n width: 90%;\n margin: 0 auto;\n margin-top: 8px;\n }\n\n .specs {\n margin-top: 20px;\n\n .spec {\n font-size: 15px;\n color: #353535;\n text-align: center;\n font-weight: 300;\n margin-bottom: 13px;\n\n .variable {\n color: #1FBEE7;\n font-weight: 400;\n }\n }\n }\n\n .btn-signup {\n margin-top: 20px;\n height: 38px;\n }\n }\n }\n}\n\n.pricing-4-options-section {\n margin-top: 55px;\n margin-bottom: 150px;\n\n .header {\n text-align: center;\n\n h3 {\n font-size: 24px;\n color: #444;\n line-height: 25px;\n font-weight: 500;\n }\n\n p {\n font-size: 16px;\n font-weight: 400;\n color: #888;\n }\n }\n\n @media(min-width: 992px) {\n .container {\n width: 1060px;\n }\n }\n\n .charts {\n margin-top: 50px;\n\n .basic-plan {\n background-color: #fff;\n border: 1px solid #E6E6E6;\n border-radius: 5px;\n box-shadow: 0px 0px 2px 0px rgba(181, 181, 181, 0.3);\n padding: 20px 30px 10px 30px; \n text-align: center;\n width: 220px;\n position: absolute;\n right: 0px;\n top: 20px;\n\n @media (max-width: 991px) {\n float: none !important;\n left: 0px !important;\n top: 0px !important;\n margin: 0 auto;\n width: 55%;\n margin-bottom: 25px !important;\n position: relative;\n margin-top: 30px;\n\n &.last {\n margin-bottom: 0px !important;\n }\n }\n\n @media (max-width: 600px) {\n width: 90% !important;\n }\n\n .plan-name {\n text-align: center;\n font-size: 17px;\n font-weight: 400;\n color: #444;\n width: 90%;\n margin: 0 auto;\n margin-top: 5px;\n }\n\n .quantity {\n text-align: center;\n border-bottom: 1px solid #d5d5d5;\n margin-top: 5px;\n\n .dollar {\n font-size: 19px;\n position: relative;\n top: -18px;\n }\n\n .price {\n font-size: 42px;\n }\n\n .period {\n font-size: 17px;\n position: relative;\n top: -8px;\n margin-left: 4px;\n }\n }\n\n .description {\n margin-top: 15px;\n font-size: 13px;\n color: #444;\n line-height: 20px;\n\n a {\n display: block;\n margin-top: 20px;\n font-weight: 500;\n font-size: 15px;\n }\n }\n }\n\n .chart {\n background-color: #fff;\n border: 1px solid #E6E6E6;\n border-radius: 5px;\n box-shadow: 0px 0px 2px 0px rgba(181, 181, 181, 0.3);\n padding: 20px 40px;\n position: relative;\n text-align: center;\n width: 280px;\n float: left;\n min-height: 315px;\n position: relative;\n top: 20px;\n\n @media(min-width: 992px) {\n &.featured {\n left: -25px;\n }\n\n &.last {\n left: -45px;\n }\n }\n\n @media (max-width: 991px) {\n float: none !important;\n left: 0px !important;\n top: 0px !important;\n margin: 0 auto;\n width: 55%;\n margin-bottom: 25px !important;\n\n &.last {\n margin-bottom: 0px !important;\n }\n }\n\n @media (max-width: 600px) {\n width: 90% !important;\n } \n\n &.featured { \n z-index: 999;\n\n @media(min-width: 992px) {\n margin: 0 auto;\n top: 0px;\n min-height: 445px;\n .btn-signup {\n margin-top: 32px;\n }\n }\n\n .popular {\n position: absolute;\n top: 0px;\n left: 0px;\n }\n\n .quantity {\n margin-top: 15px;\n padding-bottom: 5px;\n }\n }\n\n .plan-name {\n text-align: center;\n font-size: 20px;\n font-weight: 400;\n color: #444;\n width: 90%;\n margin: 0 auto;\n margin-top: 5px;\n }\n\n .quantity {\n text-align: center;\n border-bottom: 1px solid #d5d5d5;\n margin-top: 10px;\n\n .dollar {\n font-size: 19px;\n position: relative;\n top: -18px;\n }\n\n .price {\n font-size: 49px;\n }\n\n .period {\n font-size: 17px;\n position: relative;\n top: -8px;\n margin-left: 4px;\n }\n }\n\n .specs {\n margin-top: 20px;\n\n .spec {\n font-size: 15px;\n color: #353535;\n text-align: center;\n font-weight: 300;\n margin-bottom: 13px;\n\n .variable {\n color: #1FBEE7;\n font-weight: 400;\n }\n }\n }\n\n .btn-signup {\n margin-top: 20px;\n height: 38px;\n }\n } \n }\n}\n",".pricing-tabs-section {\n margin-top: 60px;\n\n .header {\n text-align: center;\n\n h3 {\n font-size: 23px;\n color: #444;\n line-height: 25px;\n font-weight: 500;\n }\n\n p {\n font-size: 16px;\n font-weight: 400;\n color: #888;\n }\n }\n\n .tabs {\n background: #F8F8F8;\n text-align: center;\n border-radius: 3px;\n margin-top: 40px;\n\n .tab {\n background-color: #F8F8F8;\n font-weight: 500;\n font-size: 15px;\n color: #555;\n display: inline-block;\n padding: 18px 25px;\n margin-top: 10px;\n cursor: pointer;\n\n @include transition(background-color 0.2s linear);\n\n &.anually {\n padding: 18px 18px 18px 23px;\n }\n\n &.active {\n background-color: #fff;\n border: 1px solid #E8E8E8;\n border-radius: 5px 5px 0px 0px;\n border-bottom: 0px;\n }\n\n span {\n position: relative;\n top: -2px;\n }\n\n small {\n color: #32ACDA;\n margin-left: 3px;\n }\n } \n }\n\n .plans {\n margin-top: 30px;\n border: 1px solid #E3E3E3;\n border-radius: 5px;\n\n .plan {\n border-right: 0px;\n min-height: 220px;\n\n &.middle {\n border-right: 1px solid #E3E3E3;\n border-left: 1px solid #E3E3E3;\n }\n\n .header {\n text-align: left;\n padding: 18px 25px;\n border-bottom: 1px solid #E3E3E3;\n margin: 0px -15px;\n font-weight: 500;\n font-size: 15px;\n\n .icon {\n color: #B7C5D8;\n font-size: 18px;\n position: relative;\n top: 2px;\n left: -4px;\n }\n\n .price {\n float: right;\n color: #3CA2E5;\n }\n }\n\n .specs {\n margin-top: 25px;\n padding: 0px 10px 17px 10px;\n\n .spec {\n font-size: 15px;\n color: #666;\n margin-bottom: 13px;\n\n .variable {\n color: #1FBEE7;\n font-weight: 400;\n }\n }\n }\n }\n }\n\n .signup {\n margin-top: 50px;\n text-align: center;\n\n .button {\n padding: 16px 38px;\n font-size: 15px;\n\n @include background-gradient(#4ABCF3, #1a80d9);\n }\n\n .message {\n text-align: center;\n margin-top: 20px;\n font-size: 12px;\n color: #888;\n }\n } \n}\n\n.pricing-comparison-section {\n .divider {\n height: 1px;\n width: 100%;\n background-color: #ddd;\n position: relative;\n margin: 120px 0 60px;\n\n &:before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 2px;\n background-image: -webkit-gradient(linear, left top, right top, color-stop(0, rgba(255, 255, 255, 0.75)), color-stop(0.5, rgba(250,250,250,0)), color-stop(1, rgba(255, 255, 255, 0.75)));\n background-image: -webkit-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: -moz-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: -o-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: -ms-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n }\n }\n\n .header {\n text-align: center;\n\n h3 {\n font-size: 24px;\n color: #444;\n line-height: 25px;\n font-weight: 500;\n }\n\n p {\n font-size: 16px;\n font-weight: 400;\n color: #888;\n }\n }\n\n .chart {\n margin-top: 40px;\n border: 1px solid #dfdfdf;\n border-radius: 5px;\n box-shadow: 0px 2px 3px 1px rgba(0, 0, 0, 0.1);\n\n .header {\n text-align: center;\n background: #fbfbfb;\n margin: 0px -15px;\n padding: 20px 35px;\n font-weight: 500;\n font-size: 17px;\n color: #444;\n border-bottom: 1px solid #dfdfdf;\n height: 73px;\n\n @media(max-width: 991px) {\n font-size: 15px;\n }\n\n @media(max-width: 767px) {\n font-size: 13px;\n }\n\n .price {\n color: #1a80d9;\n position: relative;\n top: 3px;\n font-size: 14px;\n\n @media(max-width: 767px) {\n font-size: 13px;\n }\n }\n }\n\n .features-col {\n .header {\n text-align: left;\n border-radius: 5px 0px 0px 0px;\n margin-bottom: 30px;\n padding: 23px 30px;\n font-size: 18px;\n\n @media(max-width: 991px) {\n font-size: 16px;\n }\n }\n\n .spec {\n margin-bottom: 32px;\n padding-left: 40px;\n position: relative;\n\n .icon {\n position: absolute;\n font-size: 27px;\n color: #849FC4;\n left: 0px;\n top: -4px;\n }\n }\n }\n\n .plan-col {\n border-left: 1px solid #dfdfdf;\n\n &.last {\n .header {\n border-radius: 0px 5px 0px 0px;\n }\n }\n\n .header {\n padding: 13px 0px;\n }\n\n .spec {\n margin-bottom: 34px;\n margin-top: 25px;\n\n .check {\n margin: 0 auto;\n display: block;\n max-width: 20px;\n }\n }\n\n .button {\n margin-top: 20px;\n margin-bottom: 15px;\n font-size: 13px;\n padding: 11px 24px;\n\n @include background-gradient(#4ABCF3, #1a80d9);\n\n @media(max-width: 991px) {\n padding: 6px 10px;\n text-align: center;\n }\n }\n }\n }\n\n .message {\n p {\n text-align: center;\n font-size: 13px;\n color: #656565;\n margin-top: 30px;\n }\n }\n\n .faq {\n margin-top: 50px;\n\n .header h3 {\n font-size: 20px;\n color: #454545;\n text-align: left;\n }\n\n .questions {\n margin-top: 25px;\n\n .question {\n margin-bottom: 40px;\n \n @media(min-width: 992px) {\n width: 90%;\n }\n\n strong {\n font-weight: 500;\n font-size: 16px;\n color: #454545;\n }\n\n p {\n color: #555;\n margin-top: 10px;\n font-size: 13px;\n line-height: 22px;\n }\n }\n }\n }\n}\n",".about-us-slider {\n margin-top: 60px;\n\n .header {\n text-align: left;\n\n h3 {\n font-size: 26px;\n color: #444;\n line-height: 25px;\n font-weight: 500;\n }\n\n p {\n font-size: 15px;\n font-weight: 400;\n color: #666;\n margin-top: 20px;\n line-height: 24px;\n\n @media(min-width: 992px) {\n width: 48%;\n }\n }\n }\n\n .flexslider {\n margin-top: 40px;\n box-shadow: none;\n border-radius: 0px;\n\n .slides { \n img {\n max-height: 400px;\n }\n }\n\n .flex-control-paging {\n bottom: -35px;\n\n li {\n margin: 0 7px;\n\n a {\n background: #e6e6e6;\n box-shadow: inset 0 0 2px rgba(0,0,0,0.3);\n\n @include transition(all 0.2s linear);\n\n &.flex-active {\n background: #3785b9;\n }\n }\n }\n }\n }\n}\n\n.about-us-team {\n margin-top: 40px;\n\n h1 {\n font-size: 24px;\n color: #444;\n line-height: 25px;\n font-weight: 500;\n margin-bottom: 25px;\n }\n\n p {\n color: #666;\n line-height: 23px;\n margin-bottom: 15px;\n\n @media(min-width: 992px) {\n width: 88%;\n }\n }\n\n .join-team {\n padding: 8px 26px;\n font-size: 13px;\n margin-top: 10px;\n }\n\n .stats {\n margin-top: 60px;\n font-size: 16px;\n color: #757575;\n\n strong {\n font-size: 33px;\n color: #656565;\n font-weight: 500;\n position: relative;\n top: 1px;\n margin-right: 5px;\n }\n\n .col-sm-3 {\n border-bottom: 1px solid #efefef;\n padding-bottom: 25px;\n\n @media(max-width: 767px) {\n padding-bottom: 10px;\n margin-bottom: 20px;\n }\n }\n }\n\n .team {\n margin-top: 90px;\n\n .team-row {\n text-align: center;\n margin-bottom: 35px;\n }\n\n img {\n margin: 0px 20px;\n max-width: 90px;\n border-radius: 10px;\n position: relative;\n top: 0px;\n box-shadow: 0px 2px 1px 0px rgba(0, 0, 0, 0.4);\n \n @include transition(top 0.15s linear);\n \n @media(max-width: 991px) {\n margin: 0px 10px 20px 0;\n }\n\n &:hover {\n top: -5px;\n box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.5);\n }\n }\n\n .tooltip-inner {\n padding: 12px 18px;\n font-size: 13px;\n background: rgba(0, 0, 0, .8);\n border: 1px solid #000;\n border-bottom: 0px;\n }\n\n .tooltip-arrow {\n border-top-color: rgba(0, 0, 0, .8);\n }\n }\n}\n",".contact-us-map {\n position: relative;\n top: -20px;\n box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.35);\n\n #map {\n height: 450px;\n width: 100%;\n }\n\n #directions {\n position: absolute;\n top: 50px;\n left: 13%;\n text-align: center;\n width: 300px;\n background: rgba(255, 255, 255, 0.9);\n border-radius: 5px;\n box-shadow: 0px 4px 15px 0px rgba(0, 0, 0, 0.2);\n padding: 30px 20px 30px 20px;\n\n @media(max-width: 767px) {\n display: none;\n }\n\n p {\n font-size: 19px;\n color: #333;\n margin-bottom: 15px;\n }\n\n button {\n margin-top: 15px;\n }\n }\n}\n\n.contact-us-info {\n margin-top: 50px;\n\n .message {\n h3 {\n color: #444;\n font-size: 24px;\n font-weight: 500;\n }\n\n p {\n font-size: 15px;\n font-weight: 400;\n color: #777;\n }\n\n form {\n margin-top: 35px;\n width: 80%;\n\n @media(max-width: 767px) {\n width: 100%;\n }\n \n input[type=\"text\"],\n input[type=\"email\"] {\n width: 70%;\n\n @media(max-width: 767px) {\n width: 100%;\n }\n }\n\n label {\n color: #555;\n font-weight: 500;\n margin-bottom: 7px;\n }\n\n .button {\n margin-top: 20px;\n }\n\n /* error messages from jquery validate */\n label.error {\n position: relative !important;\n display: inline-block !important;\n margin-top: -2px !important;\n margin-bottom: -8px !important;\n color: #fff !important;\n font-weight: normal !important;\n font-size: 15px !important;\n padding: 5px 7px !important;\n background: #4B9FCF !important;\n border-radius: 4px !important;\n \n &.valid {\n display: none !important;\n }\n \n &:after {\n bottom: 100%;\n border: solid transparent;\n content: \" \";\n height: 0;\n width: 0;\n position: absolute;\n pointer-events: none;\n border-bottom-color: #4B9FCF;\n border-width: 5px;\n left: 30px;\n margin-left: -5px;\n }\n }\n }\n }\n\n .contact {\n @media(max-width: 991px) {\n margin-top: 50px;\n }\n\n .address {\n margin-bottom: 30px;\n }\n\n h3 {\n color: #444;\n font-size: 18px;\n font-weight: 500;\n }\n\n p {\n font-size: 15px;\n font-weight: 400;\n color: #777;\n }\n\n .online-support {\n background: #f8fdfe;\n border: 1px solid #bee1f3;\n margin-top: 40px;\n border-radius: 6px;\n padding: 20px 22px 18px 22px;\n width: 70%;\n\n strong {\n font-weight: 500;\n color: #333;\n font-size: 15px;\n }\n\n p {\n color: #1f95d0;\n margin-top: 14px;\n line-height: 18px;\n }\n }\n\n .social {\n margin-top: 25px;\n\n a {\n border: 0px;\n\n @include transition(all 0.2s linear);\n \n &.tw {\n position: relative;\n top: 2px;\n }\n\n &:hover {\n opacity: 0.8;\n filter: alpha(opacity=80);\n }\n } \n }\n }\n}\n",".status-page {\n margin-top: 40px;\n margin-bottom: 130px;\n\n .header {\n text-align: center;\n\n h3 {\n font-size: 22px;\n color: #444;\n line-height: 25px;\n font-weight: 500;\n margin-bottom: 7px;\n }\n\n p {\n font-size: 16px;\n font-weight: 400;\n color: #888;\n }\n }\n\n .current-status {\n background: #F7F8FB;\n border: 1px solid #EAEDF7;\n border-radius: 8px;\n box-shadow: inset rgba(100, 100, 100, 0.25) 0 1px 1px;\n padding: 20px 30px;\n margin: 0 auto;\n margin-top: 35px;\n\n .updated {\n float: right;\n color: #616A72;\n font-size: 13px;\n\n @media(max-width: 767px) {\n float: none;\n text-align: center;\n display: block;\n margin-bottom: 20px;\n }\n }\n\n .status {\n font-size: 18px;\n font-weight: 500;\n\n .color {\n width: 15px;\n height: 15px; \n border-radius: 50px;\n float: left;\n position: relative;\n top: 5px;\n margin-right: 20px;\n\n &.green {\n background: #96cb4c;\n border: 1px solid #8BC43D;\n box-shadow: 0px 0px 7px 2px #B3E074, inset 0px 2px 1px -1px rgba(255, 255, 255, 0.8);\n }\n\n &.yellow {\n background: #F0D775;\n border: 1px solid #E0CC7E;\n box-shadow: 0px 0px 7px 2px #F0D775, inset 0px 2px 1px -1px rgba(255, 255, 255, 0.8);\n }\n\n &.red {\n background: #F56B65;\n border: 1px solid #FC7D77;\n box-shadow: 0px 0px 7px 2px #FF9B96, inset 0px 2px 1px -1px rgba(255, 255, 255, 0.8);\n }\n }\n }\n\n .help {\n margin-top: 7px;\n margin-left: 35px;\n }\n }\n\n .modules {\n margin-top: 40px;\n border-radius: 5px;\n\n >.list-group .list-group-item {\n padding: 13px 15px;\n\n &:first-child {\n border-top: 0px;\n border-top-right-radius: 5px;\n border-top-left-radius: 5px;\n }\n\n .status {\n float: right;\n color: #0F9B0F;\n font-size: 13px;\n font-weight: 500;\n\n &.down {\n color: #D88C1B;\n }\n }\n }\n }\n\n .messages {\n margin-top: 35px;\n\n h3 {\n font-size: 18px;\n color: #444;\n margin-bottom: 25px;\n }\n\n .date {\n margin-bottom: 25px;\n\n .day {\n font-size: 13px;\n font-weight: bold;\n color: #454545;\n padding-bottom: 6px;\n margin-bottom: 6px;\n border-bottom: 1px solid #eee;\n }\n\n .ok {\n color: #0F9B0F;\n font-size: 13px;\n font-weight: 500;\n }\n\n .issues {\n color: #D88C1B;\n font-size: 13px;\n font-weight: 500;\n margin-bottom: 18px;\n }\n\n .update {\n color: #333;\n font-size: 13px;\n\n strong {\n font-size: 13px;\n font-weight: bold;\n color: #454545;\n min-width: 93px;\n display: inline-block;\n }\n }\n }\n }\n\n .full-history {\n margin-top: 50px;\n }\n}\n",".gallery-showcase {\n margin-top: 60px;\n \n .header {\n text-align: left;\n\n h3 {\n font-size: 26px;\n color: #596166;\n line-height: 25px;\n font-weight: 500;\n }\n\n p {\n font-size: 15px;\n font-weight: 400;\n color: #666;\n margin-top: 12px;\n line-height: 24px;\n\n @media(min-width: 992px) {\n width: 48%;\n }\n }\n }\n\n .filtering {\n margin-top: 35px;\n margin-bottom: 35px;\n\n #filters {\n padding-left: 20px;\n text-align: center;\n border-bottom: 2px solid #EDEDED;\n\n @media(max-width: 767px) {\n display: none;\n }\n\n li {\n list-style-type: none;\n display: inline-block;\n margin: 0px 8px;\n\n &:first-child {\n margin-left: 0px;\n }\n\n a {\n color: #888;\n font-weight: 400;\n border: 0px;\n padding: 10px 22px;\n font-size: 16px;\n position: relative;\n\n @include transition(color 0.25s linear);\n \n &.active {\n color: #2B69A2;\n font-weight: 400;\n border-bottom: 0px;\n display: inline-block;\n\n &:after {\n content: '';\n position: absolute;\n width: 100%;\n height: auto;\n border-bottom: 2px solid #7BA3C9;\n left: 1px;\n bottom: -2px;\n }\n }\n\n &:hover {\n color: #2B69A2;\n background: none;\n text-decoration: none;\n }\n }\n }\n }\n }\n\n .gallery_container {\n overflow: visible !important;\n }\n\n .pic {\n margin-bottom: 40px;\n position: relative;\n display: block;\n\n @include transition(.5s cubic-bezier(.25,1.45,.51,1.13));\n\n &.fluidbox-opened:hover {\n .layer {\n background: rgba(0, 0, 0, 0);\n\n p {\n opacity: 0;\n filter: alpha(opacity=0);\n }\n }\n }\n\n &:hover {\n text-decoration: none;\n\n .layer {\n background: rgba(33,33,33,.8);\n\n p {\n margin-top: -20px;\n opacity: 1;\n filter: alpha(opacity=100);\n }\n }\n }\n\n .layer {\n width: 100%;\n height: 100%;\n position: absolute;\n z-index: 9;\n top: 0px;\n left: 0px;\n background: rgba(0, 0, 0, 0);\n\n @include transition(all 0.25s ease-out);\n\n p {\n transition: margin-top .35s ease-out, opacity .55s ease-out;\n color: #fff;\n font-size: 15px;\n text-align: center;\n top: 50%;\n width: 100%;\n position: absolute;\n font-weight: 600;\n margin-top: 25px;\n opacity: 0;\n filter: alpha(opacity=0);\n\n span {\n position: relative;\n top: 1px;\n margin-right: 2px;\n }\n }\n }\n\n img {\n margin: 0 auto;\n }\n }\n}\n\n/* Fluidbox styling starts here */\na[data-fluidbox] {\n background-color: #eee;\n border: none;\n cursor: -webkit-zoom-in;\n cursor: -moz-zoom-in;\n margin-bottom: 1.5rem;\n \n &.fluidbox-opened {\n cursor: -webkit-zoom-out;\n cursor: -moz-zoom-out; \n }\n \n & img {\n display: block;\n margin: 0 auto;\n opacity: 0;\n filter: alpha(opacity=0);\n max-width: 100%;\n transition: all .25s ease-in-out;\n }\n}\n\na[class^='float'] {\n margin: 1rem;\n margin-top: 0;\n width: 33.33333%;\n \n &.float-left {\n float: left;\n margin-left: 0;\n }\n\n &.float-right {\n float: right;\n margin-right: 0;\n }\n}\n\n#fluidbox-overlay {\n background-color: rgba(255,255,255,.85);\n cursor: pointer;\n cursor: -webkit-zoom-out;\n cursor: -moz-zoom-out;\n display: none;\n position: fixed;\n top: 0;\n left: 0;\n bottom: 0;\n right: 0;\n z-index: 500;\n}\n\n.fluidbox-wrap {\n background-position: center center;\n background-size: cover;\n margin: 0 auto;\n position: relative;\n z-index: 400;\n transition: all .25s ease-in-out;\n \n .fluidbox-opened & {\n z-index: 600;\n }\n}\n\n.fluidbox-ghost {\n background-size: cover;\n background-position: center center;\n position: absolute;\n transition: all .25s ease-in-out;\n}\n","html.portfolio-page {\n background: #F5F9FF;\n background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0,rgba(255,255,255,0)),color-stop(1,rgba(255,255,255,0.5)));\n background-image: -webkit-linear-gradient(top,rgba(255,255,255,0) 0,rgba(255,255,255,0.5) 100%);\n background-image: -moz-linear-gradient(top,rgba(255,255,255,0) 0,rgba(255,255,255,0.5) 100%);\n background-image: -o-linear-gradient(top,rgba(255,255,255,0) 0,rgba(255,255,255,0.5) 100%);\n background-image: -ms-linear-gradient(top,rgba(255,255,255,0) 0,rgba(255,255,255,0.5) 100%);\n background-image: linear-gradient(top,rgba(255,255,255,0) 0,rgba(255,255,255,0.5) 100%);\n -webkit-background-size: 100% 30%;\n -moz-background-size: 100% 30%;\n background-size: 100% 30%;\n background-repeat: no-repeat;\n background-position: 0 100%;\n}\n\n.portfolio-body {\n background: none;\n background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0,#fff),color-stop(25%,rgba(255,255,255,0)));\n background-image: -webkit-linear-gradient(top,#fff 0,rgba(255,255,255,0) 25%);\n background-image: -moz-linear-gradient(top,#fff 0,rgba(255,255,255,0) 25%);\n background-image: -o-linear-gradient(top,#fff 0,rgba(255,255,255,0) 25%);\n background-image: -ms-linear-gradient(top,#fff 0,rgba(255,255,255,0) 25%);\n background-image: linear-gradient(top,#fff 0,rgba(255,255,255,0) 25%);\n}\n\n.portfolio-showcase {\n margin-top: 50px;\n \n .header {\n text-align: left;\n margin-bottom: 40px;\n\n h3 {\n font-size: 28px;\n color: #596166;\n line-height: 25px;\n font-weight: 500;\n }\n\n p {\n font-size: 16px;\n font-weight: 400;\n color: #8D959B;\n line-height: 24px;\n\n @media(min-width: 992px) {\n width: 48%;\n }\n }\n }\n \n .project {\n background: #fff;\n padding: 40px 60px;\n border-radius: 5px;\n margin-bottom: 35px;\n box-shadow: 0 -1px 0 rgba(0,0,0,0.05), 0 1px 0 rgba(0,0,0,0.05), -1px 0 0 rgba(0,0,0,0.04), 1px 0 0 rgba(0,0,0,0.04), 0 1px 3px rgba(0,0,0,0.12);\n\n @media(max-width: 767px) {\n padding: 40px 35px 35px 35px;;\n }\n\n h3 {\n color: #585F64;\n font-size: 22px;\n margin-top: 0px;\n text-align: center;\n }\n\n .screen {\n margin-top: 45px;\n text-align: center;\n\n img {\n margin: 0 auto;\n }\n }\n\n .description {\n margin-top: 40px;\n color: #7D8186;\n line-height: 21px;\n }\n\n .divider {\n height: 1px;\n width: 100%;\n background-color: #ddd;\n position: relative;\n margin-top: 45px;\n\n &:before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 2px;\n background-image: -webkit-gradient(linear, left top, right top, color-stop(0, rgba(255, 255, 255, 0.75)), color-stop(0.5, rgba(250,250,250,0)), color-stop(1, rgba(255, 255, 255, 0.75)));\n background-image: -webkit-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: -moz-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: -o-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: -ms-linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n background-image: linear-gradient(left, rgba(255, 255, 255, 0.75) 0%, rgba(250,250,250,0) 50%, rgba(255, 255, 255, 0.75) 100%);\n }\n }\n\n .visit {\n margin-top: 30px;\n text-align: center;\n }\n }\n}\n",".portfolio-item-showcase {\n margin-top: 40px;\n margin-bottom: -30px;\n \n .header {\n text-align: left;\n margin-bottom: 25px;\n\n h3 {\n display: inline-block;\n font-size: 17px;\n font-family: $myriad;\n color: #53A6DA;\n margin-top: 0px;\n line-height: 25px;\n font-weight: 500;\n margin-bottom: 5px;\n cursor: pointer;\n }\n }\n \n .project {\n h1 {\n text-align: center;\n font-size: 25px;\n color: #647583;\n margin-top: 10px;\n margin-bottom: 70px;\n }\n\n .screens {\n img {\n margin: 0 auto;\n margin-bottom: 80px;\n margin-top: 10px;\n\n &.magnifier {\n width: 490px;\n margin-bottom: 100px;\n }\n\n &.mobile {\n margin-bottom: 110px;\n }\n }\n\n .text {\n color: #7D8186;\n font-size: 15px;\n line-height: 22px;\n margin-bottom: 80px;\n }\n }\n\n .description {\n .info {\n margin-top: 25px;\n\n &.stick {\n @media(min-width: 992px) {\n position: fixed;\n top: 10px;\n }\n }\n\n .icons {\n margin-bottom: 20px;\n\n .ion-icon:before {\n color: #b2c3cb;\n background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #b2c3cb), color-stop(100%, #DBE3E7));\n background: -webkit-linear-gradient(#b2c3cb, #DBE3E7);\n font-size: 60px;\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n margin: 0 9px;\n }\n\n .ion-social-windows:before {\n font-size: 50px;\n }\n }\n\n p {\n color: #7D8186;\n line-height: 22px;\n margin-bottom: 22px;\n }\n\n .tweet-it {\n color: #00A7DD;\n margin-top: 3px;\n display: inline-block;\n position: relative;\n padding-left: 29px;\n text-decoration: none;\n\n &:hover {\n .ion-social-twitter:before {\n content: \"\\f242\";\n }\n }\n\n .ion-social-twitter {\n margin-right: 8px;\n font-size: 21px;\n position: absolute;\n top: 0px;\n left: 0px;\n }\n }\n }\n }\n }\n}\n",".support-header {\n margin-top: -20px;\n background: #F4F7FA;\n border-bottom: 1px solid #DCE4EC;\n padding: 50px 0px;\n\n @media(max-width: 767px) {\n padding: 30px 0px;\n text-align: center;\n }\n\n h2, p {\n text-shadow: 1px 1px 1px #fff;\n }\n\n h2 {\n @media(max-width: 767px) {\n font-size: 27px;\n }\n\n .entypo-lifebuoy {\n position: absolute;\n left: -25px;\n top: 22px;\n font-size: 29px;\n color: #7AA3C5;\n\n @media(max-width: 767px) {\n position: relative;\n left: 0px;\n }\n }\n }\n\n p {\n font-size: 17px;\n color: #5A5A5A;\n\n @media(max-width: 767px) {\n font-size: 15px;\n }\n }\n\n form {\n margin-top: 30px;\n\n .input-group {\n &:before {\n display: inline-block;\n font-family: FontAwesome;\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n content: \"\\f002\";\n position: absolute;\n top: 14px;\n left: 13px;\n font-size: 20px;\n z-index: 99;\n color: rgb(208, 218, 228);\n }\n }\n\n .form-control {\n height: 50px;\n font-size: 17px;\n line-height: 20px;\n padding-left: 42px;\n\n @include placeholder {\n color: #BCC8D6;\n font-size: 17px;\n font-weight: 300;\n }\n }\n\n .input-group-btn {\n .btn {\n padding: 14px 18px;\n }\n }\n }\n}\n\n.support-topics {\n margin-top: 40px;\n\n h4 {\n font-weight: 400;\n margin-bottom: 50px;\n font-size: 25px;\n }\n\n .topic {\n margin-bottom: 45px;\n\n @media(max-width: 767px) {\n text-align: center; \n }\n\n .icon {\n display: block;\n font-size: 45px;\n color: #70CBF5;\n }\n\n a {\n color: #454545;\n text-decoration: none;\n @include transition(all 0.2s linear);\n\n strong {\n display: block;\n margin-top: 15px;\n font-size: 16px;\n }\n\n &:hover {\n color: #5CB9E4;\n }\n }\n\n p {\n margin-top: 17px;\n color: #838383;\n }\n }\n}\n",".not-found-page {\n height: 100%;\n background: #3A4F68;\n background: -moz-radial-gradient(center, ellipse cover, #4D6077 0%, #3A4F68 100%);\n background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, #4D6077), color-stop(100%, #3A4F68));\n background: -webkit-radial-gradient(center, ellipse cover, #4D6077 0%, #3A4F68 100%);\n background: -o-radial-gradient(center, ellipse cover, #4D6077 0%, #3A4F68 100%);\n background: -ms-radial-gradient(center, ellipse cover, #4D6077 0%, #3A4F68 100%);\n background: radial-gradient(ellipse at center, #4D6077 0%, #3A4F68 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#4D6077', endColorstr = '#3A4F68', GradientType = 1);\n}\n\n#not-found {\n overflow: hidden;\n\n .info {\n text-align: center;\n padding-top: 100px;\n\n h1 {\n color: #fff;\n font-size: 13em;\n margin-bottom: 20px;\n font-weight: 200;\n letter-spacing: -3px;\n text-shadow: 1px 1px rgba(0, 0, 0, 0.35);\n }\n\n p {\n font-size: 23px;\n color: #FFF;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.35);\n margin-bottom: 0;\n }\n\n .go-back {\n font-size: 16px;\n text-shadow: none;\n color: #9BB2CE;\n z-index: 999;\n position: absolute;\n left: 0;\n right: 0;\n padding: 25px 0;\n max-width: 400px;\n margin: auto;\n margin-top: -15px;\n\n a {\n color: #fff;\n }\n }\n }\n\n #container {\n canvas {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n right: 0;\n }\n }\n}\n\n#not-found-alt {\n overflow: hidden;\n\n .info {\n text-align: center;\n margin-top: 30px;\n position: absolute;\n width: 100%;\n\n h1 {\n color: #fff;\n font-size: 11em;\n margin-bottom: 20px;\n font-weight: 200;\n letter-spacing: -3px;\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.15);\n }\n\n p {\n font-size: 25px;\n color: #4D728A;\n margin-bottom: 0;\n }\n\n .go-back {\n font-size: 16px;\n text-shadow: none;\n letter-spacing: 0px;\n color: #506379;\n z-index: 999;\n position: absolute;\n left: 0;\n right: 0;\n padding: 25px 0;\n max-width: 400px;\n margin: auto;\n margin-top: -15px;\n\n a {\n color: #429AD5;\n text-shadow: 1px 1px 1px rgba(255, 255, 255, 0.4);\n font-size: 18px;\n }\n }\n }\n\n canvas {\n background: linear-gradient(hsl(200, 50%, 80%) 0%, hsl(200, 30%, 95%) 75%);\n display: block;\n }\n\n #container {\n height: 100%;\n left: 0;\n position: fixed;\n top: 0;\n width: 100%;\n }\n}\n",".coming-soon-page {\n border-top: 5px solid #4DB2E7;\n height: 100vh;\n background: #fff;\n background: -moz-radial-gradient(center, ellipse cover, #fff 0%, #F4F9FF 100%);\n background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%, #fff), color-stop(100%, #F4F9FF));\n background: -webkit-radial-gradient(center, ellipse cover, #fff 0%, #F4F9FF 100%);\n background: -o-radial-gradient(center, ellipse cover, #fff 0%, #F4F9FF 100%);\n background: -ms-radial-gradient(center, ellipse cover, #fff 0%, #F4F9FF 100%);\n background: radial-gradient(ellipse at center, #fff 0%, #F4F9FF 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#fff', endColorstr = '#F4F9FF', GradientType = 1); \n\n &.dark {\n border-top: 5px solid #DAEEF8;\n background: #4D576D !important;\n\n .info {\n a {\n color: #fff;\n }\n\n h3 {\n color: #fff;\n font-weight: 300;\n }\n }\n\n #countdown {\n #clock {\n .flip-clock-divider {\n .flip-clock-label {\n color: #fff;\n }\n }\n }\n }\n\n form {\n .button {\n display: inline-block;\n vertical-align: middle;\n zoom: 1;\n color: #fff;\n padding: 9px 27px;\n border: 2px solid #fff;\n border-radius: 4px;\n font-size: 15px;\n font-weight: 400;\n background: rgba(0, 0, 0, 0.08);\n text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);\n box-shadow: none;\n\n @include transition(all 0.2s linear);\n\n &:hover {\n text-decoration: none;\n color: #ddd;\n border-color: #ddd;\n }\n }\n }\n\n .social {\n .ion {\n span {\n color: #AEDCFF;\n }\n }\n }\n }\n\n .skins-nav {\n opacity: 1;\n @include transition(left .3s);\n position: fixed;\n left: -40px;\n top: 135px;\n font-size: 13px;\n z-index: 9999;\n\n &:hover {\n left: 0px;\n }\n\n a {\n display: block;\n color: #FFF;\n text-decoration: none;\n padding-right: 30px;\n height: 37px;\n border-radius: 0 4px 4px 0;\n margin-bottom: 3px;\n @include transition(all .5s);\n text-align: right;\n position: relative;\n box-shadow: inset 0 1px rgba(255, 255, 255, 0.21);\n\n &.light {\n background: #9BD4F5;\n }\n\n &.dark {\n background: #444;\n }\n\n &.active {\n .ion {\n display: block;\n }\n }\n\n .text {\n white-space: nowrap;\n display: block;\n width: 85px;\n position: relative;\n top: 9px;\n @include transition(width .2s);\n }\n\n .ion {\n position: absolute;\n top: 11px;\n right: 8px;\n display: none;\n }\n }\n }\n\n .info {\n margin-top: 60px;\n text-align: center;\n color: #fff;\n\n a {\n color: #5F5F5F;\n text-decoration: none;\n font-family: $myriad;\n font-size: 46px;\n\n &:hover {\n text-decoration: none;\n }\n }\n\n h3 {\n text-align: center;\n color: #4DB2E7;\n text-transform: uppercase;\n letter-spacing: 2px;\n font-weight: 400;\n font-size: 18px;\n margin-top: 25px;\n }\n }\n\n #countdown {\n margin-top: 35px;\n\n #clock {\n width: 100%;\n margin: 0 auto;\n\n @media(min-width: 992px) {\n width: 47%;\n }\n\n @media(max-width: 991px) {\n max-width: 142px;\n\n ul {\n margin-bottom: 50px;\n }\n\n .flip-clock-divider {\n height: inherit;\n display: block;\n\n &:before, \n &:after {\n display: table;\n content: \"\";\n }\n\n &.minutes,\n &.seconds {\n .flip-clock-label { \n right: -74px !important;\n }\n }\n\n .flip-clock-label {\n bottom: -8.5em !important;\n }\n }\n }\n\n .flip-clock-divider {\n\n &.minutes,\n &.seconds {\n .flip-clock-label { \n right: -102px;\n }\n }\n\n .flip-clock-label {\n color: #555;\n bottom: -1.5em;\n top: inherit;\n right: -92px;\n font-size: 15px;\n }\n\n .flip-clock-dot {\n display: none;\n }\n }\n }\n }\n\n form {\n margin-top: 110px;\n text-align: center;\n\n @media(max-width: 767px) {\n margin-top: 70px;\n }\n\n input[type=\"email\"] {\n width: 250px;\n height: 43px;\n\n @media(max-width: 767px) {\n margin: 0 auto;\n }\n }\n\n .button {\n @include background-gradient(#38BAFF, #37ADEB);\n padding: 10px 19px;\n font-size: 15px;\n font-weight: 500;\n border: 1px solid #54AEF5;\n }\n }\n\n .social {\n margin-top: 100px;\n text-align: center;\n margin-bottom: 20px;\n\n .ion {\n font-size: 34px;\n margin: 0 10px;\n display: inline-block;\n\n span {\n color: #9DBDD6;\n }\n\n .ion-social-twitter {\n }\n\n .ion-social-facebook {\n font-size: 25px;\n }\n }\n\n .tm {\n margin-top: 15px;\n color: #B1B9C4;\n }\n }\n}\n","#docs {\n\n\t#guide {\n\t\tbackground: #F4F8FA;\n\t\tborder-right: 1px solid #DADFE5;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\tposition: fixed;\n\t\toverflow: hidden;\n\t\toverflow-y: auto;\n\t\twidth: 210px;\n\t\tz-index: 25;\n\t\theight: 100%;\n\n\t\t.logo {\n\t\t\tmargin-top: 0;\n\t\t\tfont-size: 29px;\n\n\t\t\ta {\n\t\t\t\tcolor: #000;\n\t\t\t\tfont-family: $myriad;\n\t\t\t\tpadding: 20px 0 15px 23px;\n\t\t\t\tdisplay: block;\n\t\t\t\tborder-bottom: 1px solid #D6DCE0;\n\t\t\t\tbox-shadow: 0px 1px #FFF;\n\t\t\t\t@include transition(color .15s linear);\n\n\t\t\t\t&:hover {\n\t\t\t\t\tcolor: #858585;\n\t\t\t\t\ttext-decoration: none;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t.menu {\n\t\t\t> li {\n\n\t\t\t\t&.active {\n\t\t\t\t\t> .nav {\n\t\t\t\t\t\tdisplay: block;\n\t\t\t\t\t}\n\n\t\t\t\t\t> a {\n\t\t\t\t\t\tbackground: #DEEAF1;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\ta {\n\t\t\t\t\tcolor: #3391C9;\n\t\t\t\t\tfont-weight: 500;\n\t\t\t\t\tfont-size: 13px;\n\t\t\t\t\tpadding: 10px 15px 10px 25px;\n\n\t\t\t\t\t@include transition(all .15s linear);\n\n\t\t\t\t\t&:hover {\n\t\t\t\t\t\tbackground: #DEEAF1;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t> .nav {\n\t\t\t\t\tmargin-bottom: 7px;\n\t\t\t\t\tdisplay: none;\n\n\t\t\t\t\tli {\n\t\t\t\t\t\ta {\n\t\t\t\t\t\t\tfont-weight: 400;\n\t\t\t\t\t\t\tcolor: #48A0D6;\n\t\t\t\t\t\t\tpadding: 6px 15px 5px 35px;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\n\t#api-docs {\n\t\tbackground: #FFF;\n\t\tmargin-left: 210px !important;\n\t\tmin-width: 750px;\n\n\t\t#methods {\n\t\t\tbackground: #292E33;\n\t\t\tposition: relative;\n\t\t\tmargin-left: 50%;\n\n\t\t\t.languages {\n\t\t\t\tposition: fixed;\n\t\t\t\ttop: 0;\n\t\t\t\twidth: 100%;\n\t\t\t\tpadding: 5px 0 5px 20px;\n\t\t\t\tbackground: #3B4249;\n\t\t\t\tbox-shadow: 2px 1px 3px #000;\n\t\t\t\tz-index: 999;\n\n\t\t\t\t.language {\n\t\t\t\t\tdisplay: inline-block;\n\t\t\t\t\tcolor: #C0CEDD;\n\t\t\t\t\tpadding: 5px 15px;\n\t\t\t\t\tfont-size: 13px;\n\n\t\t\t\t\t&.selected {\n\t\t\t\t\t\tcolor: #fff;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t.method {\n\n\t\t\t\t/*&:nth-child(2) {*/\n\t\t\t\t\t/*.method-section {*/\n\t\t\t\t\t\t/*.method-description {*/\n\t\t\t\t\t\t\t/*padding-top: 0px;*/\n\t\t\t\t\t\t\t/*border-top: 0px;*/\n\t\t\t\t\t\t/*}*/\n\n\t\t\t\t\t\t/*.method-example {*/\n\t\t\t\t\t\t\t/*padding-top: 35px;*/\n\t\t\t\t\t\t\t/*border-top: 0;*/\n\t\t\t\t\t\t\t/*box-shadow: none;*/\n\t\t\t\t\t\t/*}*/\n\t\t\t\t\t/*}*/\n\t\t\t\t/*}*/\n\n\t\t\t\t&:last-child {\n\t\t\t\t\t.method-section {\n\t\t\t\t\t\tpadding-bottom: 70px;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t.method-section {\n\t\t\t\t\tmargin-left: -100%;\n\t\t\t\t\tposition: relative;\n\t\t\t\t\tpadding-top: 50px;\n\n\t\t\t\t\t.method-description {\n\t\t\t\t\t\twidth: 50%;\n\t\t\t\t\t\tfloat: left;\n\t\t\t\t\t\tpadding: 0 30px;\n\t\t\t\t\t\tborder-top: 1px solid #E5E7EB;\n\t\t\t\t\t\tpadding-top: 40px;\n\n\t\t\t\t\t\th3 {\n\t\t\t\t\t\t\tfont-size: 21px;\n\t\t\t\t\t\t\tmargin-top: 0;\n\t\t\t\t\t\t\tmargin-bottom: 20px;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tp {\n\t\t\t\t\t\t\tfont-size: 13px;\n\t\t\t\t\t\t\tcolor: #666;\n\t\t\t\t\t\t\tline-height: 21px;\n\t\t\t\t\t\t\tmargin-bottom: 20px;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t.info {\n\t\t\t\t\t\t\tmargin-top: 45px;\n\t\t\t\t\t\t\tmargin-bottom: 40px;\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t\th4 {\n\t\t\t\t\t\t\t\tfont-size: 14px;\n\t\t\t\t\t\t\t\tborder-bottom: 1px solid #E4E4E4;\n\t\t\t\t\t\t\t\tpadding-bottom: 15px;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t.field {\n\t\t\t\t\t\t\t\tmargin-top: 17px;\n\t\t\t\t\t\t\t\tfont-size: 13px;\n\n\t\t\t\t\t\t\t\t.key {\n\t\t\t\t\t\t\t\t\twidth: 40%;\n\t\t\t\t\t\t\t\t\tfloat: left;\n\t\t\t\t\t\t\t\t\ttext-align: right;\n\t\t\t\t\t\t\t\t\tpadding-right: 10px;\n\t\t\t\t\t\t\t\t\tfont-weight: bold;\n\t\t\t\t\t\t\t\t\tcolor: #333;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t.desc {\n\t\t\t\t\t\t\t\t\twidth: 60%;\n\t\t\t\t\t\t\t\t\tfloat: right;\n\t\t\t\t\t\t\t\t\tpadding-left: 10px;\n\t\t\t\t\t\t\t\t\tcolor: #555;\n\t\t\t\t\t\t\t\t\tline-height: 21px;\n\n\t\t\t\t\t\t\t\t\tstrong {\n\t\t\t\t\t\t\t\t\t\tdisplay: block;\n\t\t\t\t\t\t\t\t\t\tcolor: #333;\n\t\t\t\t\t\t\t\t\t\tmargin-bottom: 5px;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t.method-example {\n\t\t\t\t\t\tmargin-left: 50%;\n\t\t\t\t\t\twidth: 50%;\n\t\t\t\t\t\tpadding: 0px 30px 0;\n\t\t\t\t\t\tborder-top: 1px solid #000;\n\t\t\t\t\t\tbox-shadow: inset 0px 1px rgba(255, 255, 255, 0.17);\n\t\t\t\t\t\tcolor: #fff;\n\n\t\t\t\t\t\tpre {\n\t\t\t\t\t\t\tbackground: none;\n\t\t\t\t\t\t\tborder: none;\n\t\t\t\t\t\t\tmargin: 0;\n\t\t\t\t\t\t\tpadding: 20px;\n\n\t\t\t\t\t\t\tcode {\n\t\t\t\t\t\t\t\tbackground: none;\n\t\t\t\t\t\t\t\tfont-family: Monaco, Consolas, Menlo;\n\t\t\t\t\t\t\t\tfont-size: 14px;\n\n\t\t\t\t\t\t\t\t&.always-visible {\n\t\t\t\t\t\t\t\t\tdisplay: block !important;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t&.ruby {\n\t\t\t\t\t\t\t\t\tdisplay: block;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tcolor: #DAE4F2;\n\n\t\t\t\t\t\t\t\t/*> * {*/\n\t\t\t\t\t\t\t\t\t/*color: #DAE4F2;*/\n\t\t\t\t\t\t\t\t\t/*line-height: 20px;*/\n\t\t\t\t\t\t\t\t/*}*/\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t.ruby {\n\t\t\t\t\t\t\t\t/*color: #DAE4F2;*/\n\n\t\t\t\t\t\t\t\t/*> * {*/\n\t\t\t\t\t\t\t\t\t/*color: #DAE4F2;*/\n\t\t\t\t\t\t\t\t\t/*line-height: 20px;*/\n\t\t\t\t\t\t\t\t/*}*/\n\n\t\t\t\t\t\t\t\t/*[class*=keyword] {*/\n\t\t\t\t\t\t\t\t\t/*color: #9AB4DB;*/\n\t\t\t\t\t\t\t\t/*}*/\n\t\t\t\t\t\t\t\t/*[class*=string] {*/\n\t\t\t\t\t\t\t\t\t/*color: #DAD0C6;*/\n\t\t\t\t\t\t\t\t/*}*/\n\t\t\t\t\t\t\t\t/*[class*=comment] {*/\n\t\t\t\t\t\t\t\t\t/*color: #B4B4B4;*/\n\t\t\t\t\t\t\t\t/*}*/\n\t\t\t\t\t\t\t\t/*[class*=constant] {*/\n\t\t\t\t\t\t\t\t\t/*color: #FFDF9D;*/\n\t\t\t\t\t\t\t\t/*}*/\n\t\t\t\t\t\t\t\t/*[class*=symbol] {*/\n\t\t\t\t\t\t\t\t\t/*color: #9ECBEE;*/\n\t\t\t\t\t\t\t\t/*}*/\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t.python {\n\t\t\t\t\t\t\t\tcolor: #DAE4F2;\n\n\t\t\t\t\t\t\t\t> * {\n\t\t\t\t\t\t\t\t\tcolor: #DAE4F2;\n\t\t\t\t\t\t\t\t\tline-height: 20px;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t[class*=keyword] {\n\t\t\t\t\t\t\t\t\tcolor: #9AB4DB;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t[class*=string] {\n\t\t\t\t\t\t\t\t\tcolor: #DAD0C6;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t[class*=comment] {\n\t\t\t\t\t\t\t\t\tcolor: #B4B4B4;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t[class*=class] {\n\t\t\t\t\t\t\t\t\tcolor: #FFDF9D;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t[class*=params] {\n\t\t\t\t\t\t\t\t\tcolor: #9ECBEE;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t.php {\n\t\t\t\t\t\t\t\tcolor: #DAE4F2;\n\n\t\t\t\t\t\t\t\t> * {\n\t\t\t\t\t\t\t\t\tcolor: #DAE4F2;\n\t\t\t\t\t\t\t\t\tline-height: 20px;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t[class*=keyword] {\n\t\t\t\t\t\t\t\t\tcolor: #9AB4DB;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t[class*=string] {\n\t\t\t\t\t\t\t\t\tcolor: #DAD0C6;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t[class*=comment] {\n\t\t\t\t\t\t\t\t\tcolor: #B4B4B4;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\t\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n\n",".invoice-wrapper {\n background: #eeeff1;\n padding-top: 60px;\n\n .container {\n @media (min-width: 1200px) {\n width: 900px;\n }\n }\n\n .logo {\n text-align: center;\n\n a {\n font-size: 36px;\n color: #555;\n font-weight: 300;\n\n @include transition(all .2s linear);\n\n &:hover {\n text-decoration: none;\n color: #7793C4;\n }\n }\n }\n\n .invoice-wrapper {\n background: #FFF;\n border: 1px solid #CDD3E2;\n box-shadow: 0px 0px 1px #CCC;\n padding: 40px 40px 60px;\n margin-top: 40px;\n border-radius: 4px;\n\n .intro {\n line-height: 25px;\n color: #444;\n }\n\n .payment-info {\n margin-top: 25px;\n padding-top: 15px;\n\n span {\n color: #A9B0BB;\n }\n\n strong {\n display: block;\n color: #444;\n margin-top: 3px;\n }\n\n @media(max-width: 767px) {\n .text-right {\n text-align: left;\n margin-top: 20px;\n }\n }\n }\n\n .payment-details {\n border-top: 2px solid #EBECEE;\n margin-top: 30px;\n padding-top: 20px;\n line-height: 22px;\n\n span {\n color: #A9B0BB;\n display: block;\n }\n\n a {\n display: inline-block;\n margin-top: 5px;\n }\n\n @media(max-width: 767px) {\n .text-right {\n text-align: left;\n margin-top: 20px;\n }\n }\n }\n\n .line-items {\n margin-top: 40px;\n\n .headers {\n color: #A9B0BB;\n font-size: 13px;\n letter-spacing: .3px;\n border-bottom: 2px solid #EBECEE;\n padding-bottom: 4px;\n }\n\n .items {\n margin-top: 8px;\n border-bottom: 2px solid #EBECEE;\n padding-bottom: 8px;\n\n .item {\n padding: 10px 0;\n color: #696969;\n font-size: 15px;\n\n @media(max-width: 767px) {\n font-size: 13px;\n }\n\n .amount {\n letter-spacing: 0.1px;\n color: #84868A;\n font-size: 16px;\n\n @media(max-width: 767px) {\n font-size: 13px;\n }\n }\n }\n }\n\n .total {\n margin-top: 30px;\n\n .extra-notes {\n float: left;\n width: 40%;\n text-align: left;\n font-size: 13px;\n color: #7A7A7A;\n line-height: 20px;\n\n @media(max-width: 767px) {\n width: 100%;\n margin-bottom: 30px;\n float: none;\n }\n\n strong {\n display: block;\n margin-bottom: 5px;\n color: #454545;\n }\n }\n\n .field {\n margin-bottom: 7px;\n font-size: 14px;\n color: #555;\n\n &.grand-total {\n margin-top: 10px;\n font-size: 16px;\n font-weight: 500;\n\n span {\n color: #20A720;\n font-size: 16px;\n }\n }\n\n span {\n display: inline-block;\n margin-left: 20px;\n min-width: 85px;\n color: #84868A;\n font-size: 15px;\n }\n }\n }\n\n .print {\n margin-top: 50px;\n text-align: center;\n\n a {\n display: inline-block;\n border: 1px solid #9CB5D6;\n padding: 13px 13px;\n border-radius: 5px;\n color: #708DC0;\n font-size: 13px;\n\n @include transition(all .2s linear);\n\n &:hover {\n text-decoration: none;\n border-color: #333;\n color: #333;\n }\n\n i {\n margin-right: 3px;\n font-size: 14px;\n }\n }\n }\n }\n }\n\n .footer {\n margin-top: 40px;\n margin-bottom: 110px;\n text-align: center;\n font-size: 12px;\n color: #969CAD;\n }\n}\n",".timeline-bg {\n background: #f7f8fa;\n}\n\n.timeline-wrapper {\n margin-top: 60px;\n position: relative;\n\n &:before {\n content: '';\n position: absolute;\n width: 5px;\n height: 100%;\n left: 0;\n background: #e7e8ec;\n right: 0;\n margin: 0 auto;\n z-index: -1;\n }\n\n .year {\n text-align: center;\n margin-bottom: 50px;\n font-weight: 600;\n margin-top: 50px;\n font-size: 16px;\n\n &:nth-child(1) {\n margin-top: 0px;\n margin-bottom: 60px;\n }\n\n &.last {\n margin-bottom: 0;\n font-size: 21px;\n }\n\n span {\n display: inline-block;\n background: #E7E8EC;\n padding: 6px 20px;\n color: #5A5A5A;\n border-radius: 3px;\n }\n }\n\n /* the first node that comes right next to a year */\n .year + .node {\n margin-top: -20px;\n }\n\n .node {\n position: relative;\n float: right;\n clear: right;\n width: 44%;\n margin: 35px 0;\n border-radius: 5px;\n\n @media(max-width: 767px) {\n width: 100%;\n }\n\n &.left {\n float: left;\n clear: left;\n\n &:before {\n right: -51px;\n left: inherit;\n\n @media(max-width: 991px) {\n right: -42px;\n width: 43px;\n }\n }\n\n .marker {\n left: inherit;\n right: -65px;\n\n @media(max-width: 991px) {\n right: -52px;\n }\n }\n }\n\n &:before {\n content: '';\n position: absolute;\n width: 50px;\n height: 4px;\n background: #E7E8EC;\n left: -51px;\n top: 62px;\n\n @media(max-width: 991px) {\n left: -42px;\n width: 43px;\n }\n\n @media(max-width: 767px) {\n display: none;\n }\n }\n\n .marker {\n position: absolute;\n width: 17px;\n height: 17px;\n border-radius: 25px;\n background: #e7e8ec;\n left: -65px;\n top: 55px;\n border: 3px solid #F7F8FA;\n\n @media(max-width: 991px) {\n left: -51px;\n }\n\n @media(max-width: 767px) {\n display: none;\n }\n }\n\n .entry {\n position: relative;\n padding: 20px;\n height: 400px;\n background-size: cover;\n background-position: center center;\n border-radius: 5px;\n box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.26);\n\n &:before {\n position: absolute;\n top: 50%;\n bottom: 0;\n left: 0;\n right: 0;\n content: '';\n border-radius: 0 0 5px 5px;\n background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.89) 100%);\n background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.89) 100%);\n background: -o-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.89) 100%);\n background: linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(15, 24, 49, 0.89) 100%);\n }\n\n &.smallish {\n height: 320px;\n\n .intro {\n top: 68%;\n }\n }\n\n &.short-entry {\n background: #E7E8EC;\n color: #5c6269;\n height: inherit;\n padding: 13px;\n box-shadow: none;\n position: relative;\n top: 41px;\n left: -1px;\n margin-bottom: 20px;\n text-shadow: 1px 1px rgba(255, 255, 255, 0.4);\n\n &:before {\n background: none;\n }\n }\n\n .intro {\n position: relative;\n top: 75%;\n color: #FFF;\n z-index: 9;\n letter-spacing: .3px;\n\n h4 {\n font-size: 20px;\n text-shadow: 1px 1px rgba(0, 0, 0, 0.69);\n letter-spacing: .5px;\n\n @media(max-width: 767px) {\n font-size: 17px;\n }\n }\n\n p {\n font-weight: 300;\n\n @media(max-width: 767px) {\n font-size: 13px;\n }\n }\n }\n }\n\n .news {\n position: relative;\n background-size: cover;\n background-position: center center;\n border-radius: 5px;\n box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.26);\n background: #fff;\n\n section {\n padding: 30px 20px 60px 20px;\n\n h3 {\n margin-top: 0;\n font-size: 17px;\n line-height: 22px;\n }\n\n p {\n margin-top: 21px;\n line-height: 22px;\n font-size: 13px;\n color: #7A7979;\n }\n }\n\n footer {\n position: absolute;\n bottom: 0;\n padding: 12px 20px;\n width: 100%;\n\n font-size: 12px;\n font-weight: 500;\n color: #9EAEBE;\n\n .tag {\n position: relative;\n padding-left: 27px;\n text-transform: uppercase;\n\n span {\n font-size: 24px;\n position: absolute;\n top: -4px;\n left: 0;\n }\n }\n }\n }\n }\n}\n"],"file":"main.css"} \ No newline at end of file diff --git a/website/static/1.13/bin/bundle b/website/static/1.13/bin/bundle new file mode 100755 index 00000000..50da5fdf --- /dev/null +++ b/website/static/1.13/bin/bundle @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../Gemfile", __dir__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_requirement + @bundler_requirement ||= + env_var_version || + cli_arg_version || + bundler_requirement_for(lockfile_version) + end + + def bundler_requirement_for(version) + return "#{Gem::Requirement.default}.a" unless version + + bundler_gem_version = Gem::Version.new(version) + + bundler_gem_version.approximate_recommendation + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/website/static/1.13/bin/jekyll b/website/static/1.13/bin/jekyll new file mode 100755 index 00000000..d2e20b59 --- /dev/null +++ b/website/static/1.13/bin/jekyll @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'jekyll' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("jekyll", "jekyll") diff --git a/website/static/1.13/bin/kramdown b/website/static/1.13/bin/kramdown new file mode 100755 index 00000000..2da4ffe3 --- /dev/null +++ b/website/static/1.13/bin/kramdown @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'kramdown' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("kramdown", "kramdown") diff --git a/website/static/1.13/bin/listen b/website/static/1.13/bin/listen new file mode 100755 index 00000000..613171d2 --- /dev/null +++ b/website/static/1.13/bin/listen @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'listen' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("listen", "listen") diff --git a/website/static/1.13/bin/rake b/website/static/1.13/bin/rake new file mode 100755 index 00000000..4eb7d7bf --- /dev/null +++ b/website/static/1.13/bin/rake @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rake' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rake", "rake") diff --git a/website/static/1.13/bin/rougify b/website/static/1.13/bin/rougify new file mode 100755 index 00000000..ffe119f6 --- /dev/null +++ b/website/static/1.13/bin/rougify @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'rougify' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("rouge", "rougify") diff --git a/website/static/1.13/bin/safe_yaml b/website/static/1.13/bin/safe_yaml new file mode 100755 index 00000000..d4b9332d --- /dev/null +++ b/website/static/1.13/bin/safe_yaml @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'safe_yaml' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("safe_yaml", "safe_yaml") diff --git a/website/static/1.13/bin/sass b/website/static/1.13/bin/sass new file mode 100755 index 00000000..177336b2 --- /dev/null +++ b/website/static/1.13/bin/sass @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'sass' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("sass-embedded", "sass") diff --git a/website/static/1.13/bin/sass-convert b/website/static/1.13/bin/sass-convert new file mode 100755 index 00000000..a24043ae --- /dev/null +++ b/website/static/1.13/bin/sass-convert @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'sass-convert' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("sass", "sass-convert") diff --git a/website/static/1.13/bin/scss b/website/static/1.13/bin/scss new file mode 100755 index 00000000..8ab54108 --- /dev/null +++ b/website/static/1.13/bin/scss @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'scss' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("sass", "scss") diff --git a/website/static/1.13/blog.html b/website/static/1.13/blog.html new file mode 100644 index 00000000..ef4fa96d --- /dev/null +++ b/website/static/1.13/blog.html @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +
+

Here you can keep up-to-date with Graphiti, including updates, +deprecations, and our future roadmap. If there’s anything you’d like +to read about, let us know on Discord

+
+ +

Tutorial Write-Ups Are Out!

+

Though we’ve long had a sample application with step-by-step diffs, we’ve been missing a full walkthrough. That now exists! Start with Step 0: Bootstrapping.

+
+ +

Graphiti 1.2 Released

+

I’m thrilled to announce much-needed, much-improved Rails integration, +courtesy of the amazing Peter Wagenet and +the new graphiti-rails gem. +This gives us better error handling, tighter controllers, and a solid +foundation for the codebase moving forward.

+
+ +

Graphiti 1.1 Released

+

Our first minor version bump centers around an important update: +better errors for clients. This is the first phase of work to allow us to provide more actionable errors to API users who might not also be developers of the API itself. This was brought to us by Graphiti maintainer Wade Tandy.

+
+ +

Graphiti 1.0 Released 🎉

+

This project has gone through a number of iterations over the past three years. I’m happy to say we’ve now released 1.0, committing to semantic versioning. Graphiti has now seen enough use, across a wide variety of scenarios, that we can commit to no backwards-incompatible changes for a long while, and a changelog for future releases.

+
+ +
+ +


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

+ +
+
+
+ + + + + + diff --git a/website/static/1.13/cheatsheet.html b/website/static/1.13/cheatsheet.html new file mode 100644 index 00000000..74d8d3ca --- /dev/null +++ b/website/static/1.13/cheatsheet.html @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Resource Cheatsheet

+ +

This is an expanded version of what’s “under the hood” of a default +Resource:

+ +
class EmployeeResource < ApplicationRecord
+  self.model = Employee
+
+  # JSONAPI type
+  # http://jsonapi.org/format/#document-resource-identifier-objects
+  self.type = :employees
+
+  # Expanded version
+  attribute :name, :string,
+    readable: self.attributes_sortable_by_default,
+    writable: self.attributes_sortable_by_default,
+    sortable: self.attributes_sortable_by_default,
+    filterable: self.attributes_sortable_by_default
+
+  # Alter display
+  # @object is your model instance
+  attribute :name, :string do
+    @object.name.upcase
+  end
+
+  # Default nil
+  self.default_sort = [{ name: :desc }]
+  # Default 10
+  self.default_per_page = 10
+
+  # Custom sort
+  # sort :name, :string if no attribute defined
+  sort :name do |scope, dir|
+    scope.order(name: dir)
+  end
+
+  # Custom Filter
+  # filter :name, :string if no attribute defined
+  filter :name do
+    # All of the operators here have not_ equivalents, e.q. not_eq
+    # imagine ".where.not" instead of ".where"
+
+    eq do |scope, value|
+      scope.where("lower(name) IN ?", value.map(&:downcase))
+    end
+
+    eql do |scope, value|
+      scope.where(name: value)
+    end
+
+    prefix do |scope, value|
+      value.each do |v|
+        scope = scope.where('lower(name) LIKE ?', "#{v.downcase}%")
+      end
+      scope
+    end
+
+    suffix do |scope, value|
+      value.each do |v|
+        scope = scope.where('lower(name) LIKE ?', "%#{v.downcase}")
+      end
+      scope
+    end
+
+    match do |scope, value|
+      value.each do |v|
+        scope = scope.where('lower(name) LIKE ?', "%#{v.downcase}%")
+      end
+      scope
+    end
+  end
+
+  # Operators for integer, float, datetime, etc
+  filter :age, :integer do
+    eq do |scope, value|
+      scope.where(age: value)
+    end
+
+    gt do |scope, value|
+      value.each do |v|
+        scope = scope.where('age > ?', v)
+      end
+      scope
+    end
+
+    gte do |scope, value|
+      value.each do |v|
+        scope = scope.where('age >= ?', v)
+      end
+      scope
+    end
+
+    lt do |scope, value|
+      value.each do |v|
+        scope = scope.where('age < ?', v)
+      end
+      scope
+    end
+
+    lte do |scope, value|
+      value.each do |v|
+        scope = scope.where('age <= ?', v)
+      end
+      scope
+    end
+  end
+
+  # Will be passed to sort, filter, etc
+  # Apply global logic here: only return active Employees,
+  # filter results based on the current user, etc
+  def base_scope
+    Employee.all
+  end
+
+  # Must execute query and return an array of Model instances
+  def resolve(scope)
+    scope.to_a
+  end
+
+  def create(attributes)
+    employee = Employee.create(attributes)
+    employee.save
+    employee
+  end
+
+  def update(attributes)
+    employee = self.class.find(id: attributes.delete(:id))
+    employee.update_attributes(attributes)
+    employee
+  end
+
+  def destroy(id)
+    employee = self.class.find(id: attributes.delete(:id))
+    employee.destroy
+    employee
+  end
+end
+ + +
+
+
+ + + + + + diff --git a/website/static/1.13/cookbooks/authorization.md b/website/static/1.13/cookbooks/authorization.md new file mode 100644 index 00000000..e69de29b diff --git a/website/static/1.13/cookbooks/caching.md b/website/static/1.13/cookbooks/caching.md new file mode 100644 index 00000000..e69de29b diff --git a/website/static/1.13/cookbooks/customizing-sideloads.html b/website/static/1.13/cookbooks/customizing-sideloads.html new file mode 100644 index 00000000..83143d5d --- /dev/null +++ b/website/static/1.13/cookbooks/customizing-sideloads.html @@ -0,0 +1,325 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Customizing Sideloads

+ +
+

See the code in our sample app

+
+ +

This cookbook will help you understand sideloading. It would be great to +live in a world where everything follows default ActiveRecord table +conventions, but in my experience this is rarely the case. From legacy +code to alternate datastores, we need to think in Real World terms.

+ +

Our Employee Directory sample application +has a clean schema - let’s screw with it. Let’s say Department has a +column called watcher_emails, which is an array of strings. We want to +sideload Department > Watchers. Though the relationship is called +watchers, these will be Employee records.

+ +

Let’s start by adding a spec:

+ +
# spec/resources/department/reads_spec.rb
+
+describe 'sideloading' do
+  describe 'watchers' do
+    let!(:employee1) { create(:employee) }
+    let!(:employee2) { create(:employee) }
+    let!(:employee3) { create(:employee) }
+    let!(:department) do
+      create :department,
+        watcher_emails: [employee1.email, employee3.email]
+    end
+
+    before do
+      params[:include] = 'watchers'
+    end
+
+    it 'sideloads employees via watcher_emails' do
+      render
+      sl = d[0].sideload(:watchers)
+      expect(sl.map(&:id)).to eq([employee1.id, employee3.id])
+      expect(sl.map(&:jsonapi_type).uniq).to eq(['employees'])
+    end
+  end
+end
+ +

Add the relationship:

+ +
# app/resources/department_resource.rb
+has_many :watchers, resource: EmployeeResource
+ +

Run the test and you’ll get this error:

+ +
Graphiti::Errors::AttributeError:
+  EmployeeResource: Tried to filter on attribute :department_id, but could not find an attribute with that name.
+ +

How would we track down this error? Well, we know Resources connect +together with Links. Let’s +take a look at the query parameters that would be used to connect these +two Resources:

+ +
has_many :watchers, resource: EmployeeResource do
+  params do |hash, departments|
+    binding.pry
+  end
+end
+ +
+

Note - we’re using pry to debug here.

+
+ +

The value of hash here is:

+ +
{ filter: { department_id: "1" } }
+ +

Which makes sense. If we say has_many :things, by default we expect +Thing to have a department_id we can query.

+ +

That’s not our case, though. Instead, let’s customize those parameters +to fit our use case:

+ +
params do |hash, departments|
+  emails = departments.map(&:watcher_emails).flatten
+  hash[:filter] = { email: emails }
+end
+ +

Instead of querying by department_id, we need to query by email. And +the value we pass in will be an array of email addresses

+ +

We’d need to add an email filter to EmployeeResource to make this +work. This gets us querying correctly, but there’s another error:

+ +
undefined method `department_id' for #<Employee:0x00007f9652ae6d80>
+ +

Here’s the thing to keep in mind: let’s say our request was +/departments?include=watchers. We queried all the data, and we now +have an array of Departments and an array of Employees. Now we need +to specify which employees should be assigned as watchers of which +department.

+ +

Let’s write that code manually:

+ +
has_many :watchers, resource: EmployeeResource do
+  # ... code ...
+  assign do |departments, employees|
+    departments.each do |d|
+      d.watchers = employees.select do |e|
+        e.email.in?(d.watcher_emails)
+      end
+    end
+  end
+end
+ +

We’re selecting all relevant Employees for a given Department by +checking the array of watcher_emails.

+ +

This code can be tightened up a little with assign_each (recommended). +This way we don’t have to iterate departments or worry about the +assignment ourselves:

+ +
has_many :watchers, resource: EmployeeResource do
+  # ... code ...
+
+  assign_each do |department, employees|
+    employees.select { |e| e.email.in?(d.watcher_emails) }
+  end
+end
+ +

Note that we’re using #select to return an array of relevant +Employees. If this was a belongs_to or has_one relationship, we’d +probably want to use #find to return a single Employee.

+ +

OK there’s one last error:

+ +
undefined method `watchers=' for #<Department:0x00007feb625a7468>
+ +

This one is simple - the assign function will call your Adapter’s +assignment logic, which by default will be a simple department.watchers += relevant_employees. That means we need to add a getter/setter for +this property:

+ +
# app/models/department.rb
+attr_accessor :watchers
+ +

And we’re done! The test should now pass. Check out the working code +here.

+ +


+

+ +
+
+
+ + + + + + diff --git a/website/static/1.13/cookbooks/etags.md b/website/static/1.13/cookbooks/etags.md new file mode 100644 index 00000000..e69de29b diff --git a/website/static/1.13/cookbooks/hopping-relationships.html b/website/static/1.13/cookbooks/hopping-relationships.html new file mode 100644 index 00000000..62936db3 --- /dev/null +++ b/website/static/1.13/cookbooks/hopping-relationships.html @@ -0,0 +1,324 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Hopping Relationships

+ +
+

See the code

+
+ +

Our sample application +has the setup Employee > Position > Department, where one of the +positions is the current_position. What if we wanted to change this to +Employee > Department, hiding everything about positions +under-the-hood?

+ +

Let’s start by saying an Employee has many Departments. Here’s the +spec:

+ +
describe 'sideloading' do
+  describe 'departments' do
+    let!(:employee) { create(:employee) }
+    let!(:position1) do
+      create :position,
+        historical_index: 2,
+        employee: employee,
+        department: department1
+    end
+    let!(:position2) do
+      create :position,
+        historical_index: 1,
+        employee: employee,
+        department: department2
+    end
+    let!(:department1) { create(:department) }
+    let!(:department2) { create(:department) }
+
+    before do
+      params[:include] = 'departments'
+    end
+
+    it 'finds the departments for all positions' do
+      render
+      sl = d[0].sideload(:departments)
+      expect(sl.map(&:id)).to eq([department1.id, department2.id])
+      expect(sl.map(&:jsonapi_type).uniq).to eq(['departments'])
+    end
+  end
+end
+ +

Start by defining the association:

+ +
has_many :departments
+ +

And you’ll get this error:

+ +
Graphiti::Errors::AttributeError:
+  DepartmentResource: Tried to filter on attribute :employee_id, but could not find an attribute with that name.
+ +

Which makes sense - if this is a has_many association, we’d expect +DepartmentResource to filter by employee_id. Though in our case we +don’t have that as a foreign key, we can still implement the +employee_id filter:

+ +
filter :employee_id, :integer, only: [:eq] do
+  eq do |scope, value|
+    scope.joins(:positions).merge(Position.where(employee_id: value))
+  end
+end
+ +

In order to find Departments by an employee_id, we need to join the +positions table which has the employee_id column.

+ +

We now get this error:

+ +
NoMethodError:
+  undefined method `employee_id' for #<Department:0x00007fa6f330f768>
+ +

Let’s say our URL is /employees?include=departments. We’ve fetched all +the Employees and all the Departments, now we need to associate each +Department with its relevant Employee. Normally we’d do that by +looking at the employee_id foreign key on Department, but this +scenario has non-standard logic. Let’s tell Graphiti how to select +relevant Departments for a given Employee:

+ +
has_many :departments do
+  assign_each do |employee, departments|
+    departments.select do |d|
+      employee_ids = d.positions.map(&:employee_id).flatten
+      employee.id.in?(employee_ids)
+    end
+  end
+end
+ +

There’s one final step - because we’re assigning a department to an +employee, we have to make sure that accessor exists:

+ +
# app/models/employee.rb
+attr_accessor :department
+ +

And that’s it! Our test now passes.

+ +

There’s a little bit of sleight-of-hand above though. Our filter joins +to the positions table, and our assignment iterates over departments +and calls department.positions. If we don’t eager load, we’ll cause +an N+!!

+ +

There are two solutions to this. The first is to simple change .joins +to .eager_load:

+ +
scope.eager_load(:positions).merge(Position.where(employee_id: value))
+ +

This ensures that not only are we joining on the positions table, +we’ll eagler load the positions relationship and avoid the N+1.

+ +

If you’re a stickler, though, you may have a nitpick. For one, if +we’re hitting /departments?filter[employee_id] directly there is no +need to eager load positions because we’re never associating to an +Employee. We’re paying a performance penalty when we don’t have to.

+ +

OK, let’s keep our filter .joins. We just have to tell Graphiti to +switch it to .eager_load when sideloading through EmployeeResource:

+ +
has_many :departments do
+  # ... code ...
+
+  pre_load do |proxy, employees|
+    proxy.scope.object = proxy.scope.object.eager_load(:positions)
+  end
+end
+ +

The pre_load hook fires after we’ve built up the scope, but before we +resolve it (before actually firing the query). It yields a proxy +object that we can modify - here we’re modifying the scope to eager load +positions.

+ +

It’s up to you if you care about this scenario - you may want to start +with .eager_load and only embrace to the extra work of pre_load when +you really need it.

+ +

The trick to these customizations is to think in Links. Resources +connect to each other with URLs - what would the query parameters of the +URL be? In this case, filter?[employee_id]=123. After that, we just +have to define how to associate relevant objects. Even with complex +associations hopping several levels, the same logic applies.

+ +

See the final code here.

+ +


+

+ +
+
+
+ + + + + + diff --git a/website/static/1.13/cookbooks/json_attributes.md b/website/static/1.13/cookbooks/json_attributes.md new file mode 100644 index 00000000..e69de29b diff --git a/website/static/1.13/cookbooks/openstruct-models.md b/website/static/1.13/cookbooks/openstruct-models.md new file mode 100644 index 00000000..e69de29b diff --git a/website/static/1.13/cookbooks/remote-resources.md b/website/static/1.13/cookbooks/remote-resources.md new file mode 100644 index 00000000..e69de29b diff --git a/website/static/1.13/cookbooks/without-activerecord.html b/website/static/1.13/cookbooks/without-activerecord.html new file mode 100644 index 00000000..64570871 --- /dev/null +++ b/website/static/1.13/cookbooks/without-activerecord.html @@ -0,0 +1,510 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Usage Without ActiveRecord

+ +

Graphiti was built to be used with any ORM or datastore, from PostgreSQL +to elasticsearch to Net::HTTP. In fact, Graphiti itself is tested with +Plain Old Ruby Objects (POROs).

+ +

This cookbook will show how to customize a resource around a particular datastore, and how to package those +customizations into a reusable adapter. We’ll use an in-memory datastore +and Plain Old Ruby Objects (POROs) here, but the lessons apply to any +datastore.

+ +

For working code, see this branch of the sample application.

+ +

We’ll start with this PORO model:

+ +
class Post
+  # Define getters/setters
+  # e.g. post.title = 'foo'
+  ATTRS = [:id, :title]
+  ATTRS.each { |a| attr_accessor(a) }
+
+  # Instantiate with hash of attributes
+  # e.g. Post.new(title: 'foo')
+  def initialize(attrs = {})
+    attrs.each_pair { |k,v| send(:"#{k}=", v) }
+  end
+
+  # This part only needed for our particular
+  # persistence implementation; you may not need it
+  # e.g. post.attributes # => { title: 'foo' }
+  def attributes
+    {}.tap do |attrs|
+      ATTRS.each do |name|
+        attrs[name] = send(name)
+      end
+    end
+  end
+end
+ +

And this in-memory datastore:

+ +
# If we were working with more than just Posts, we'd need a 'type'
+# field here as well, to simulate a table name.
+DATA = [
+  { id: 1, title: 'Graphiti' },
+  { id: 2, title: 'is' },
+  { id: 3, title: 'super' },
+  { id: 4, title: 'dope' }
+]
+ + + +

+ Resource Overrides +

+
+ +

If it’s your first time with a new ORM or datastore, we recommend +putting the logic in the Resource first. Once things are working and +there are multiple uses of the same overrides, package them into an +Adapter.

+ +
class PostResource < ApplicationResource
+  self.adapter = Graphiti::Adapters::Null
+
+  attribute :title, :string
+
+  def base_scope
+    {}
+  end
+
+  def resolve(scope)
+    DATA.map { |d| Post.new(d) }
+  end
+end
+ +

Here we’re using the Null adapter, which acts as a dumb pass-through. +This can be helpful when you just want to get running for a simple use +case and don’t want errors around features you haven’t implemented yet. +But it can also be confusing when you expect certain codepaths to +be hit. Mostly just be aware of Null’s behavior, or use +Graphiti::Adapters::Abstract to get helpful errors around what’s not +implemented.

+ +

We’re also supplying an explicit base_scope. This is the beginning +query object we’ll modify as params come in. In the case of +ActiveRecord, we might want an ActiveRecord::Relation like +Post.all. For our example, we’ll modify a simple ruby hash (keep in +mind the premise of building a hash of options and passing it off to a +client can apply to any datastore).

+ +

Finally, we’re resolving that scope, +returning the full dataset for now. The contract of #resolve is to +return an array of model instances, hence DATA.map { |d| Post.new(d) +}.

+ + + +

+ Sorting +

+
+ +
sort_all do |scope, attribute, direction|
+  scope[:sort].merge!(attribute: att, direction: dir)
+end
+
+def base_scope
+  { sort: {} }
+end
+
+def resolve(scope)
+  if sort = scope[:sort].presence
+    data = DATA.sort_by { |d| d[sort[:attribute].to_sym] }
+    data = data.reverse if sort[:direction] == :desc
+  end
+  DATA.map { |d| Post.new(d) }
+end
+ +

We modified the base scope with a default hash key, :sort. When the +user requests sorting, we record this by merging into the hash. We can +then reference that information on the scope when resolving.

+ +

Note the sort_all scope block, in fact all scope blocks, must return the scope.

+ + + +

+ Paginating +

+
+ +
paginate do |scope, current_page, per_page|
+  scope.merge!(current_page: current, per_page: per)
+end
+
+def resolve(scope)
+  # ... sorting ...
+  start = (scope[:current_page] - 1) * scope[:per_page]
+  stop  = start + scope[:per_page]
+  data  = data[start...stop]
+  # ... return models ...
+end
+ +

Again: merge into the scope, then reference the scope data when +resolving.

+ + + +

+ Filtering +

+
+ +
filter :title, only: [:eq] do
+  eq do |scope, value|
+    scope[:filters][attribute] = value
+    scope
+  end
+end
+
+def base_scope(*)
+  { sort: {}, filters: {} }
+end
+
+def resolve(scope)
+  # ... sorting ...
+  scope[:filters].each_pair do |k, v|
+    data = data.select { |d| d[k.to_sym].in?(v) }
+  end
+  # ... pagination ...
+  # ... return models ...
+end
+ +

Same as above examples. Again, note that we must return the scope object +from the filter function.

+ + + +

+ Persisting +

+
+ +

All at once:

+ +
# Instantiate a model for #create
+def build(model_class)
+  model_class.new
+end
+
+# Used for create/update
+def assign_attributes(model, attributes)
+  attributes.each_pair do |k, v|
+    model.send(:"#{k}=", v)
+  end
+end
+
+# Used for create/update
+def save(model)
+  attrs = model.attributes.dup
+  attrs[:id] ||= DATA.length + 1
+  if existing = DATA.find { |d| d[:id].to_s == attrs[:id].to_s }
+    existing.merge!(attrs)
+  else
+    DATA << attrs
+  end
+  model
+end
+
+# Used for destroy
+def delete(model)
+  DATA.reject! { |d| d[:id].to_s == model.id.to_s }
+  model
+end
+ +

These are the overrides for persistence operations. You are encouraged +not to override create/update/destroy directly and instead use +Persistence Lifecycle Hooks.

+ + + +

+ Adapters +

+
+ +

OK so we have all our read and write operations working correctly. But +if we had multiple Resources all using an in-memory datastore, you’d see +this logic repeated all over the place. Let’s create an adapter to DRY +up this logic.

+ +

There isn’t much more to do than copy/paste what we’ve already done. +Let’s start with our base_scope, sorting, and pagination:

+ +
class POROAdapter < Graphiti::Adapters::Abstract
+  def base_scope(*)
+    { sort: {}, filters: {} }
+  end
+
+  def paginate(scope, current, per)
+    scope.merge!(current_page: current, per_page: per)
+  end
+
+  def order(scope, att, dir)
+    scope[:sort].merge!(attribute: att, direction: dir)
+    scope
+  end
+
+  def resolve(scope)
+    data = DATA
+    if sort = scope[:sort].presence
+      data = data.sort_by { |d| d[sort[:attribute].to_sym] }
+      data = data.reverse if sort[:direction] == :desc
+    end
+    start = (scope[:current_page] - 1) * scope[:per_page]
+    stop  = start + scope[:per_page]
+    data  = data[start...stop]
+
+    data.map { |d| resource.model.new(d) }
+  end
+end
+ +

There’s really nothing here we haven’t seen before. We’re taking the +code we originally wrote, and sticking it into the interface defined by +Graphiti::Adapters::Abstract.

+ +

There’s a little more to do with filtering:

+ +
def filter(scope, attribute, value)
+  scope[:filters][attribute] = value
+  scope
+end
+alias :filter_string_eq :filter
+alias :filter_integer_eq :filter
+alias :filter_date_eq :filter
+# ... etc ...
+ +

The logic is the same, but we have a separate method for each filter +operator. This allows us to query differently based on the type - for +instance, ActiveRecord will default to case-insensitive for strings, but +straight equality for integers. If you don’t need operator-specific +logic, just alias as you see here.

+ +

You may want to limit the default operators we expect to work with a +given type. Let’s say your backend allows straight equality for strings, +but doesn’t support prefix, suffix, etc. You can specify this in +your adapter:

+ +
def self.default_operators
+  super.tap do |built_in|
+    built_in[:string] = [:eq]
+  end
+end
+
+# or avoid super altogether
+
+def self.default_operators
+  {
+    string: [:eq],
+    integer: [:eq]
+    # ... etc ...
+  }
+end
+ +

That’s it for reads. For writes, I’ll post the entire adapter code +below - again, it’s just copy/pasting what we already wrote into a +slightly different format.

+ +
def destroy(model)
+  Post::DATA.reject! { |d| d[:id].to_s == model.id.to_s }
+  model
+end
+
+def save(model)
+  attrs = model.attributes.dup
+  attrs[:id] ||= Post::DATA.length + 1
+  if existing = Post::DATA.find { |d| d[:id].to_s == attrs[:id].to_s }
+    existing.merge!(attrs)
+  else
+    Post::DATA << attrs
+  end
+  model
+end
+
+# For wrapping persistence operations in a DB transactions
+# Our in-memory DB doesn't have transactions, so just yield
+def transaction(*)
+  yield
+end
+ +

That’s really it. See the working code in Employee Directory here.

+ +
+
+
+ + + + + + diff --git a/website/static/1.13/features.html b/website/static/1.13/features.html new file mode 100644 index 00000000..fa0d5f69 --- /dev/null +++ b/website/static/1.13/features.html @@ -0,0 +1,249 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+

Formats to showcase features

+

Choose your favorite formats for your own sites

+
+
+
+ + +
+
+
+

You don't need to have any advanced technical

+

+ Whether you want to fill this paragraph with some text like I'm doing right now, this place is perfect to describe some features or anything you want - React has a complete solution for you. +

+

+ You have complete control over the look & feel of your website, we offer the best quality so you take your site up and running in no time. +

+
+
+ picture1 +
+
+
+
+ picture2 +
+
+

You don't need to have any advanced technical

+

+ Whether you want to fill this paragraph with some text like I'm doing right now, this place is perfect to describe some features or anything you want - React has a complete solution for you. +

+
+
+
+
+

You don't need to have any advanced technical

+

+ Whether you want to fill this paragraph with some text like I'm doing right now, this place is perfect to describe some features or anything you want - React has a complete solution for you. +

+

+ You have complete control over the look & feel of your website, we offer the best quality so you take your site up and running in no time. +

+
+
+ picture3 +
+
+
+
+ picture4 +
+
+

You don't need to have any advanced technical

+

+ Whether you want to fill this paragraph with some text like I'm doing right now, this place is perfect to describe some features or anything you want - React has a complete solution for you. +

+
+
+
+
+
+
+
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/feed.xml b/website/static/1.13/feed.xml new file mode 100644 index 00000000..7806ea83 --- /dev/null +++ b/website/static/1.13/feed.xml @@ -0,0 +1,106 @@ +Jekyll2026-08-06T14:52:03-05:00/1.13/feed.xmlGraphitiStylish Graph APIs +Tutorial Write-Ups Are Out!2019-10-14T00:00:00-05:002019-10-14T00:00:00-05:00/1.13/2019/10/14/tutorialThough we’ve long had a sample application with step-by-step diffs, we’ve been missing a full walkthrough. That now exists! Start with Step 0: Bootstrapping.

+ +

These write-ups will tell you the relevant code and commands, but +they’ll also give helpful context around the decisions Graphiti makes. +Even if you’re already a Graphiti user, I suggest checking it out!

+ +


+
+
+
+
+
+
+
+
+
+

]]>
Lee Richmond
Graphiti 1.2 Released2019-05-20T00:00:00-05:002019-05-20T00:00:00-05:00/1.13/2019/05/20/graphiti-1-2I’m thrilled to announce much-needed, much-improved Rails integration, +courtesy of the amazing Peter Wagenet and +the new graphiti-rails gem. +This gives us better error handling, tighter controllers, and a solid +foundation for the codebase moving forward.

+ +

You’ll be especially happy about this release if you’ve ever tried to +add error-handling middleware. Because prior versions of Graphiti used +rescue_from in ApplicationController, we intercepted errors before +other middleware had access to them. Now everything just works.

+ +

As part of this upgrade, we’re switching from GraphitiErrors to +RescueRegistry. Think of +RescueRegistry as a well-refactored version of GraphitiErrors, available to +even non-Graphiti developers.

+ +

Though you don’t have to do anything to upgrade to Graphiti 1.2, this release will start throwing deprecation notices around ApplicationController. Don’t worry, the required changes are quite minor and spelled out in the graphiti-rails migration guide.

+ +

I’m quite happy with how this effort turned out. This release dramatically improves our integration with Rails internals, and I think everyone involved learned a lot. I’m honored to work with such talented developers in our open-source community ❤️

+ +

If you have any further questions, check out the #dev or #rails +channels in our Discord Chat

+ +


+
+
+
+
+
+
+
+
+
+

]]>
Lee Richmond
Graphiti 1.1 Released2019-05-08T00:00:00-05:002019-05-08T00:00:00-05:00/1.13/2019/05/08/graphiti-1-1Our first minor version bump centers around an important update: +better errors for clients. This is the first phase of work to allow us to provide more actionable errors to API users who might not also be developers of the API itself. This was brought to us by Graphiti maintainer Wade Tandy.

+ +

Previously, when a client sent a bad write request to the server - +unwritable attributes/relationships, or bad types - we would render the +same generic 500 error JSON. We now render helpful error messages, so +third-party clients know how to correct their requests.

+ +

This is currently only affecting invalid writes, but the foundation laid +will be extendable to reads as well.

+ +


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

]]>
Lee Richmond
Graphiti 1.0 Released 🎉2019-03-31T00:00:00-05:002019-03-31T00:00:00-05:00/1.13/2019/03/31/graphiti-1-0This project has gone through a number of iterations over the past three years. I’m happy to say we’ve now released 1.0, committing to semantic versioning. Graphiti has now seen enough use, across a wide variety of scenarios, that we can commit to no backwards-incompatible changes for a long while, and a changelog for future releases.

+ +

We’ll be writing more in this blog about our future roadmap. Stay tuned!

+ +


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

]]>
Lee Richmond
\ No newline at end of file diff --git a/website/static/1.13/guides/concepts/backends-and-models.html b/website/static/1.13/guides/concepts/backends-and-models.html new file mode 100644 index 00000000..22b1383d --- /dev/null +++ b/website/static/1.13/guides/concepts/backends-and-models.html @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ + + +

+ Overview +

+
+ +

A Resource queries and persists to a Backend. It returns +Models from the Backend response, which get serialized. In this way, it is +an implementation of the Repository Pattern.

+ +

This is best illustrated in code. Let’s say we have a Backend class +that accepts a hash of options to perform a query:

+ +
results = Backend.query \
+  conditions: { name: 'Jane' },
+  sort: { created_at: :asc }
+
+results # [{ id: 1, name: 'Jane', rank: 83 }, ...]
+ +

And a PORO Model that encapsulates those results, holding business logic:

+ +
class Employee
+  attr_accessor :id, :name, :rank
+
+  def initialize(attrs = {})
+    attrs.each_pair { |k, v| send(:"#{k}=", v) }
+  end
+
+  def exemplary?
+    rank > 80
+  end
+end
+ +

Meaning that normally, our code would look something like:

+ +
results = Backend.query(params)
+employees = results.map { |r| Employee.new(r) }
+employees.map(&:exemplary?) # => [true, false, ...]
+ +

Let’s wire-up that same code to a Resource:

+ +
class EmployeeResource < ApplicationResource
+  # We'll be coding the logic manually
+  self.adapter = Graphiti::Adapters::Null
+
+  attribute :name, :string
+
+  # The blank scope we start with
+  def base_scope
+    { conditions: {}, sort: {}  }
+  end
+
+  # Merge filters into the hash based on request params
+  filter :name do
+    eq do |scope, value|
+      scope[:conditions].merge!(value)
+      scope
+    end
+  end
+
+  # Set sort based on request params
+  sort :name do |scope, direction|
+    scope[:sort] = { name: direction }
+    scope
+  end
+
+  # 'scope' here is our hash
+  # We pass it to Backend.query, and return Models
+  def resolve(scope)
+    results = Backend.query(scope)
+    results.map { |r| Employee.new(r) }
+  end
+end
+ +

As you see above, a scope can be anything from an +ActiveRecord::Relation to a plain Ruby Hash. We want to adjust +something based on the request parameters and pass it to our backend. +From the raw backend results, we can instantiate Models. Note that we +always return the full scope at the end of each block.

+ +

Of course, most Backends have predictable and consistent interfaces. It +would be a pain to manually write this code for every Resource. So +instead we could build an Adapter to DRY this logic:

+ +
class EmployeeResource < ApplicationResource
+  self.adapter = BackendAdapter
+  attribute :name, :string
+end
+ +

In summary: a Resource builds a query that is sent to a Backend. The +backend executes the query, and we instantiate Models from the raw +results.

+ + + +

+ ActiveRecord +

+
+ +

From the ActiveRecord Guides:

+ +
+

Active Record was described by Martin Fowler in his book Patterns of Enterprise Application Architecture. In Active Record, objects carry both persistent data and behavior which operates on that data. Active Record takes the opinion that ensuring data access logic as part of the object will educate users of that object on how to write to and read from the database.

+
+ +

In other words, ActiveRecord combines a Backend and Model. +Opinions on this vary, +but Graphiti supports either approach: we can separate data and business layers, or +combine them. See the ActiveRecord doppelgänger of the above at our +Resource cheatsheet.

+ + + +

+ Model Requirements +

+
+ +

The only hard requirement of a Model is that it responds to id. We use +model.id to determine uniqueness when rendering a JSONAPI response. +You will get incorrect results if model.id is not unique.

+ +

Models should also respond to any readable attributes. Remember that:

+ +
attribute :name, :string
+ +

Is the same as

+ +
# @object is your Model instance
+attribute :name, :string do
+  @object.name
+end
+ +

If your Model does not respond to #name, either pass a block to attribute or +look into aliasing.

+ + + +

+ Validations +

+
+ +

Graphiti will perform validations on your models during write requests, +returning a JSONAPI-compliant errors payload. +To get this functionality, your model must adhere to the +ActiveModel::Validations API:

+ +
model.valid?
+object.errors.messages.each_pair { ... }
+ +

It is highly recommended to mix in:

+ +
class Employee
+  include ActiveModel::Validations
+end
+ + + +

+ Model Implementations +

+
+ +

Because our default is ActiveRecord, it may be unclear what other Models +look like. Graphiti itself has no opinion about your Model layer, but +below are a few examples.

+ + + +

+ 2.1 PORO +

+
+ +
class Employee
+  attr_accessor :id,
+    :first_name,
+    :last_name,
+    :age
+
+  def initialize(attrs = {})
+    attrs.each_pair { |k,v| send(:"#{k}=", v) }
+  end
+end
+ +

This is a common Ruby example. attr_accessor defines getters and +setters for our properties, and we assign those properties in the +constructor:

+ +
e = Employee.new(id: 1, first_name: 'Jane')
+e.first_name # => 'Jane'
+ + + +

+ 2.2 ActiveModel::Model +

+
+ +

A simple abstraction of the above is ActiveModel::Model:

+ +
class Employee
+  include ActiveModel::Model
+
+  attr_accessor :id,
+    :first_name,
+    :last_name,
+    :age
+end
+ +
e = Employee.new(id: 1, first_name: 'Jane')
+e.first_name # => 'Jane'
+ + + +

+ 2.3 Dry::Struct +

+
+ +

dry-types is a dependency of Graphiti and successor to the popular Virtus.

+ +
module Types
+  include Dry::Types.module
+end
+
+class Employee < Dry::Struct
+  attribute :id, Types::Integer
+  attribute :first_name, Types::String
+  attribute :last_name, Types::Integer
+  attribute :age, Types::Integer
+end
+ +
e = Employee.new(id: 1, first_name: 'Jane')
+e.first_name # => 'Jane'
+ + + +

+ 3 Model Tips +

+
+ + + +

+ ID-less Models +

+
+ +

If your Model does not have an id property, using a random UUID is +perfectly acceptable:

+ +
def id
+  @id ||= SecureRandom.uuid
+end
+ +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/concepts/debugging.html b/website/static/1.13/guides/concepts/debugging.html new file mode 100644 index 00000000..f66dd4f1 --- /dev/null +++ b/website/static/1.13/guides/concepts/debugging.html @@ -0,0 +1,440 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ + + +

+ Debugger +

+
+ +

Graphiti comes with a debugger that shows the queries executed for a +given request. Remember that Resources have a query interface independent of a request or response. And Resources connect similar to ActiveRecord’s includes:

+ +
employees = EmployeeResource.all
+PositionResource.all(filter: { employee_id: employees.map(&:id) })
+ +
+

Remember, this is all customizable.

+
+ +

That means we can log the requests made by individual Resources:

+ +

/api/v1/employees?include=notes,positions.department.teams

+

+ +

+ +

And even copy/paste these queries into a console session to debug:

+ +
$ bin/rails c
+>> TeamResource.all({:filter=>{:department_id=>"1,2,3"}})
+ +

If you’re having trouble with a request, see if you can isolate to a +specific Resource, then test that Resource directly.

+ +

Finally: if an error occurs, we’ll note the query that caused it:

+ +

+ +

+ + + +

+ JSON Output +

+
+ +

It can be helpful to have this debug output come back as part of the +JSON response. To enable this:

+ +
# app/controllers/application_controller.rb
+def allow_graphiti_debug_json?
+  true
+  # or, current_user.admin?
+  # or, Rails.env.development?
+end
+ +

And request the debug output:

+ +

/your/url?debug=true

+ +

You should now see the debug output in meta:

+ +

+ +

+ +


+ +

If there’s an error, and you’ve enabled raw errors, you’ll also see the query that caused the error in the JSON response:

+ +


+ +

+ +

+ +


+ + + +

+ 1.2 Configuration +

+
+ +

By default, we’ll log to Rails.logger, and only enable debugging (logs +or JSON) when Rails.logger.level is set to debug. Here are the +various ways to configure.

+ +

Use config.debug to explicitly toggle debugging:

+ +
# config/initializers/graphiti.rb
+Graphiti.configure do |c|
+  c.debug = false
+end
+
+# Or use environment variable
+# GRAPHITI_DEBUG=false
+ +

Use config.debug_models to get additional (but verbose) output:

+ +

+ +

+ +
# config/initializers/graphiti.rb
+Graphiti.configure do |c|
+  c.debug_models = true
+end
+
+# Or use environment variable
+# GRAPHITI_DEBUG_MODELS=true
+ +

As noted above, allow_graphiti_debug_json? must return true if you +want JSON output:

+ +
# app/controllers/application_controller.rb
+def allow_graphiti_debug_json?
+  true
+  # or, current_user.admin?
+  # or, Rails.env.development?
+end
+ +

Note you need to explicitly pass ?debug=true in the request.

+ +

Assign a different logger:

+ +
Graphiti.logger = Logger.new(...)
+
+# Or the built-in STDOUT logger:
+Graphiti.logger = Graphiti.stdout_logger
+ +

Manually apply the debugging (when using Rails, this normally happens in +a around_action):

+ +
Graphiti::Debugger.debug do
+  EmployeeResource.all
+end
+ + + +

+ 2 Rake Tasks +

+
+ +

There are some common debugging scenarios that are possible to do +manually, but their frequency warrants common patterns. For these, we +have rake tasks.

+ + + +

+ 2.1 graphiti:request +

+
+ +
+

bin/rake graphiti:request[PATH,DEBUG]

+
+ +

Execute a request using ActionDispatch::Integration::Session (which +underlies request specs).

+ +

This can be helpful when you don’t have, or don’t want to spin up, a web +server. Imagine you want to debug something on production, so you shell +into a docker container and edit some files locally. Now you want to +execute a request and see if your changes worked:

+ +
$ bin/rake graphiti:request[/employees]
+ +

Will execute the request and spit out the JSON response. You may want to +run with the Debugger enabled:

+ +
$ bin/rake graphiti:request[/employees,true]
+ +

Which add Debugger output as well.

+ +

The PATH should not contain the domain unless you want to hit a live +API instead of a test server.

+ + + +

+ 2.2 graphiti:benchmark +

+
+ +
+

bin/rake graphiti:benchmark[PATH,NUM_REQUESTS]

+
+ +

It can be helpful to run a quick benchmark without hitting a live web +server, to eliminate the vagaries of latency. To do this:

+ +
$ bin/rake graphiti:benchmark[/employees,100]
+ +

Which will return the average response time.

+ + + +

+ 2.3 Authorization headers +

+
+ +

If you have an Authorization scheme implemented (for example authenticate_or_request_with_http_token in rails) you can supply the Authorization http header value with the AUTHORIZATION_HEADER environment variable:

+ +
$ export AUTHORIZATION_HEADER="Token --PRIVATE_API_KEY--"
+$ bin/rake graphiti:request[/employees,true]
+ +

This also will work for Basic (request_http_basic_authentication) and Bearer values

+ + + +

+ 3 Tips +

+
+ +

When debugging an application, try to isolate the individual Resource +call and debug the Resource directly (instead of running the entire +request). This helps eliminate variables, and plain ruby code is easier +to work with. If possible, try to remove Graphiti entirely and focus on +your Models and Backends.

+ +

The most common scenario is debugging a query. We suggest overriding +resolve and using pry (or equivalent):

+ +
# Introspect the scope without firing a query
+# Call 'super' to fire the query
+def resolve(scope)
+  binding.pry
+end
+ +


+

+ +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/concepts/endpoints.html b/website/static/1.13/guides/concepts/endpoints.html new file mode 100644 index 00000000..3b2fb605 --- /dev/null +++ b/website/static/1.13/guides/concepts/endpoints.html @@ -0,0 +1,432 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+

Endpoints

+ + + +
+ +
+ + + +

+ 1 Overview +

+
+ +

Endpoints expose and customize +Resources.

+ +

It’s important to remember that Resources themselves can operate +completely independently of a request or response:

+ +
employees = EmployeeResource.all({
+  filter: { title: 'engineer' },
+  sort: '-created_at',
+  page: { size: 10 },
+  include: 'positions.department'
+})
+
+employees.map(&:first_name) # => ['Jane', 'John', ...]
+employees.to_json # => { employees: [{ ... }] }
+ +

And Resources connect to other Resources. Our graph of data is defined +outside of the actual API.

+ +

Endpoints expose this graph to the world. We might choose to have a /employees +endpoint that can eager load comments (?include=comments), but never expose +/comments directly. Or, we could do the opposite: expose lazy-loading /comments, +but disallow eager loading from /employees. We can add caching rules, +or add an /exemplary_employees endpoint with special query overrides.

+ +

Finally, Endpoints are in charge of the HTTP specification: +request processing, response codes, caching, MIME types, and so on. If you’re thinking +Rails, an Endpoint is the combination of a Route and Controller.

+ + + +

+ Endpoint Logic +

+
+ +

Often, you won’t need to customize Endpoints - especially if you’re +using our Rails Resource +generator. Endpoint logic mostly +concerns:

+ +
    +
  • Caching
  • +
  • Side-effect behavior specific to the endpoint (e.g.: sending a +welcome email from /users#create but not /admin/users#create)
  • +
  • Authorization (e.g before_action)
  • +
  • Custom query parameter handling
  • +
  • Validation handling
  • +
  • Error handling
  • +
  • Limiting Resource behavior
  • +
  • Customizing Resource behavior
  • +
+ +

If your logic falls elsewhere, consider a Resource or Model.

+ + + +

+ 1.2 Rails Integration +

+
+ +

When using Rails, an endpoint is the combination of a Route and +Controller:

+ +
# config/routes.rb
+resources :posts, only: [:index]
+
+# app/controllers/posts_controller.rb
+class PostsController < ApplicationController
+  def index
+    posts = PostResource.all(params)
+    respond_with(posts)
+  end
+end
+ +

You’ll note that Graphiti hooks into Rails with a mixin (set when using +our application generator):

+ +
class ApplicationController < ActionController::API
+  include Graphiti::Rails
+
+  # ... code ...
+end
+ +

This gives us #sideload_allowlist and sets the +context.

+ + + +

+ 2 Customizing Resources +

+
+ + + +

+ Scope Overrides +

+
+ +

One common use case for endpoints is customizing the Resource +base scope. This causes a new +“starting point” for query building.

+ +

Consider the endpoints /posts (basic CRUD) and /top_posts. Though +both are associated to PostResource, /top_posts ensures that only +Posts with a certain number of upvotes get returned:

+ +
def index
+  base_scope = Post.where("upvotes > ?", 100)
+  posts = PostResource.all(params, base_scope)
+  respond_with(posts)
+end
+ +

We’re able to reuse all the other logic in PostResource - relationships, +filters, sorts, etc - while only returning “Top Posts”.

+ + + +

+ Sideload Allowlist +

+
+ +

Resources define relationships to other resources. But we may not want +all of those relationships exposed at a given endpoint.

+ +

Let’s say we’ve defined relationships:

+ +

Employee > Position > Department > Hardware > CostHistory

+ +

It’s reasonable to get an Employee, their Positions, and Departments for +those positions in a single request. But is it really valid to also pull down +all the hardware, as well as all the historical data on the cost of that hardware, +in a single request? Allowing the entire graph to be pulled down in a single request can cause excessive load on our +servers (and this is probably a better fit for lazy-loading via +Links).

+ +

Let’s instead say that if we’re entering the graph at /employees, the +furthest we can go is Department:

+ +
class EmployeesController < ApplicationController
+  self.sideload_allowlist = {
+    index: { positions: 'department' }
+  }
+
+  # ... code ...
+end
+ + + +

+ Caching +

+
+ + + +

+ Etags +

+
+ +

ETags are an important concept that is often overlooked. Etags tell browsers +that the response to a GET request hasn’t changed since the last request and +can be safely pulled from the browser cache. If you care about sparse fieldsets, +you should care about ETags - if you’re limiting fields to reduce payload size, +how about a payload size of zero?

+ +

It’s important to note that ETags are set by default in Rails, by +checking the response body. This won’t prevent queries from executing, +but it will save clients from downloading the response again if nothing +has changed.

+ +

Let’s manually set an ETag:

+ +
def index
+  posts = PostResource.all(params)
+
+  if stale?(posts.data)
+    respond_with(posts)
+  end
+end
+ +

From the documentation on #stale?:

+ +
+

In this case last_modified will be set by calling maximum(:updated_at) on the collection (the timestamp of the most recently updated record) and the etag by passing the object itself.

+
+ +

Also consider the use case where data is ingested hourly. We can avoid a +query altogether by checking when the last ingestion ran:

+ +
def index
+  if stale?(EmployeeIngestion.last)
+    employees = EmployeeResource.all(params)
+    respond_with(employees)
+  end
+end
+ +
+

CAVEAT: When setting ETags, consider sideloads. In the above examples +we are checking to see the last update of an Employee, but we may be +sideloading (and filtering) Positions as well. Use custom endpoints or +Sideload Allowlist to mitigate this issue.

+
+ + + +

+ 4 Testing +

+
+ +

If you have custom Endpoint logic, we suggest testing using an API +Test.

+
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/concepts/error-handling.html b/website/static/1.13/guides/concepts/error-handling.html new file mode 100644 index 00000000..215e3fb1 --- /dev/null +++ b/website/static/1.13/guides/concepts/error-handling.html @@ -0,0 +1,396 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+

Error Handling

+ + + +
+ +
+ + + +

+ 1 Overview +

+
+ +

Whenever we have an application error, we want to respond with a +JSONAPI-compliant errors payload. +This way clients have a predictable response detailing information about +the error.

+ +

+ +

+ +

We’ll also need a way to customize this payload. For instance, if a +NotAuthorized error is raised, the response should have a 403 status +code. For other errors, we may want to render a helpful error message:

+ +
class ApplicationController < ActionController::API
+  register_exception NotAuthorized, status: 403
+  register_exception ShipmentDelayed,
+    detail: ->(e) { "Contact us at 123-456-7899" }
+  # ... code ...
+end
+ +

Exception handling lives in Graphiti’s Rails integration. Customizing the behavior based on error class happens in the RescueRegistry dependency.

+ + + +

+ 1.1 Setup +

+
+ +

Include the Rails integration in the controllers serving your resources:

+ +
class ApplicationController < ActionController::Base
+  include Graphiti::Rails::Controller
+end
+ +

That registers handlers for Graphiti’s own exceptions and renders anything else as JSON:API. register_exception itself is available on every controller without it — see below.

+ + + +

+ 1.1.1 Displaying Raw Errors +

+
+ +

+ +

+


+ +

It can be useful to display the raw error as part of the JSON response - +but you probably don’t want to expose your stack trace to customers. +Let’s only show raw errors for the staging environment:

+ +
class ApplicationController < ActionController::API
+  # ... code ...
+
+  def show_detailed_exceptions?
+    Rails.env.staging?
+  end
+end
+ +

Another common pattern is to only show raw errors when the user is +privileged to see them:

+ +
class ApplicationController < ActionController::API
+  # ... code ...
+
+  def show_detailed_exceptions?
+    current_user.admin?
+  end
+end
+ +

When #show_detailed_exceptions? returns true, you’ll get the raw error class, +message, and backtrace in the JSON response.

+ + + +

+ 2 Usage +

+
+ + + +

+ 2.1 Basic +

+
+ +

Let’s register an error with a custom response code:

+ +
register_exception Errors::NotAuthorized, status: 403
+ +

Now if we raise Errors::NotAuthorized, the response code will be +403.

+ +

Additional options:

+ +
register_exception Errors::NotAuthorized,
+  status: 403,
+  title: "You cannot perform this action",
+  detail: :exception, # render the raw error message
+  detail: ->(error) { "Invalid Action" } # message via proc
+ +

See full documentation in the RescueRegistry README.

+ +

All controllers will inherit any registered exceptions from their parent. They can also add their own. In this example, FooError will only throw a custom status code when thrown from FooController:

+ +
class FooController < ApplicationController
+  register_exception FooError, status: 422
+end
+ + + +

+ 2.2 Advanced +

+
+ +

The final option register_exception accepts is handler. Here you can inject your own error handling class that customize RescueRegistry::ExceptionHandler. For example:

+ +
class MyCustomHandler < Graphiti::Rails::ExceptionHandler
+  # self.exception accessible within all instance methods
+
+  def status_code
+    # ...customize...
+  end
+
+  def error_code
+    # ...customize...
+  end
+
+  def title
+    # ...customize...
+  end
+
+  def detail
+    # ...customize...
+  end
+
+  def meta
+    # ...customize...
+  end
+end
+
+register_exception FooError, handler: MyCustomHandler
+ +

If you would like to use the same custom handler for all errors, override default_exception_handler:

+ +
# app/controllers/application_controller.rb
+def self.default_exception_handler
+  MyCustomHandler
+end
+ + + +

+ 3 Testing +

+
+ +

This pattern of globally rescuing exceptions makes sense when +running our live application…but during testing, we may want to +raise real errors and bypass this rescue logic.

+ +

This is why we turn off error-handling during tests by default:

+ +
# spec/rails_helper.rb
+RSpec.configure do |config|
+  config.include Graphiti::Rails::TestHelpers
+  # ... code ...
+
+  config.before :each do
+    handle_request_exceptions(false)
+  end
+end
+ +

If you want to turn this on for an individual test (so you can test +error codes, etc):

+ +
before do
+  handle_request_exceptions(true)
+end
+ +


+

+ +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/concepts/links.html b/website/static/1.13/guides/concepts/links.html new file mode 100644 index 00000000..abec3e67 --- /dev/null +++ b/website/static/1.13/guides/concepts/links.html @@ -0,0 +1,501 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +

+ +

+ + + +

+ 1 Overview +

+
+ +

Links are useful for:

+ +
    +
  • Discoverability
  • +
  • Lazy-loading
  • +
  • Hiding implementation details
  • +
+ +

Let’s say we’re loading a Post and its “Top Comments”. On Day One, we +might eager load the data like so:

+ +

/posts/123?include=top_comments

+ +

This fetches all the data in a single request. But after some UI +testing, we decide to add a “show comments” button. This way our +page can load more quickly - it only needs to load the Post +initially, and loading Top Comments can be deferred. We +want to lazy load the relationship.

+ +

How would we do this? We could bake this logic into our next request:

+ +

/comments?filter[post_id]=123&filter[upvotes][gte]=100

+ +

But this requires the client to have knowledge of what a “Top Comment” +is. If this logic ever changed, we’d have to update every client - our +desktop app, mobile apps, reports, etc… Not to mention, third parties who +just want to display Top Comments are required to have this knowledge +and update their implementations as well.

+ +

Maybe we could hide what “Top Comment” means with a special endpoint:

+ +

/top_comments?filter[post_id]=123

+ +

But now clients need to know to hit this special endpoint instead of +the normal /comments endpoint. How would they know? What if +top_comments had special caching rules and shouldn’t be used for +this purpose?

+ +

The main problem here is there is no way to guarantee our lazy-loaded data will +match our eager loaded data. Whether we fetch the Post and its Top +Comments in a single request, or lazy-load that data in a separate +request, the same data should always be returned.

+ +

+ +

+ +

Links solve this problem. When we fetch the Post, the top_comments +relationship will contain a URL. Clients can simply follow that URL to +lazy-load the same data. We can now change the definition of a Top +Comment - 500 upvotes, factor in recency, apply downvotes - and no +clients need to change. They simple continue to follow a Link.

+ + + +

+ 1.1 Linking Relationships +

+
+ +

When defining a relationship, we get a Link for free:

+ +
class PostResource < ApplicationResource
+  has_many :comments
+end
+ +
+

/comments?filter[post_id]=123

+
+ +

And when customizing a relationship with params, our Link will be +updated:

+ +
has_many :comments do
+  params do |hash|
+    hash[:filter][:upvotes] = { gte: 100 }
+  end
+end
+ +
+

/comments?filter[post_id]=123&filter[upvotes][gte]=100

+
+ +

Note: if you use the scope block directly, it may cause incorrect +links. Avoid using scope directly and instead use params and +pre_load if possible.

+ +

To manually generate a Link:

+ +
has_many :comments do
+  link do |post|
+    helpers = Rails.application.routes.url_helpers
+    helpers.comments_url(params: { filter: { post_id: post.id } })
+    # or
+    # http://example.com/api/v1/comments?filter[post_id]=123
+  end
+end
+ +

To avoid a Relationship Link altogether:

+ +
has_many :comments, link: false
+ + + +

+ 2 Resource Endpoints +

+
+ +

To generate links, we need to associate a Resource to a URL. By default, +this happens automatically:

+ +
class ApplicationResource < Graphiti::Resource
+  # ... code ...
+  self.endpoint_namespace = '/api/v1'
+end
+
+class PostResource < ApplicationResource
+  # under the hood:
+  primary_endpoint 'posts',
+    [:index, :show, :create, :update, :destroy]
+end
+ +

Which would generate links to /api/v1/posts.

+ + + +

+ 2.1 Validation +

+
+ +

Associating a Resource to an Endpoint serves two purposes. We’ve gone +over link generation. But we also want to make sure we’re not linking to +something that doesn’t actually exist. That’s why we perform Endpoint +Validation.

+ +

If we tried to access the above resource at a /comments endpoint:

+ +
class CommentsController < ApplicationController
+  def index
+    PostResource.all(params)
+    # ...
+  end
+end
+ +

We’d get a Graphiti::Errors::InvalidEndpoint error. Endpoint +validation ensures that our auto-generated Links are actually valid.

+ +

To change the endpoint associated to a Resource:

+ +
primary_endpoint 'special_posts', [:index, :show]
+ +

Or to alter only the path:

+ +
self.endpoint[:path] = 'special_posts'
+ +

Or to alter only the actions supported:

+ +
self.endpoint[:actions] = [:index, :show]
+ +

A resource may be accessible by multiple endpoints. Maybe PostResource +is also used at /top_posts. We want to keep all auto-generated links +pointing to /posts (the primary endpoint), but allow accessing +PostResource from the /top_posts endpoint:

+ +
secondary_endpoint '/top_posts', [:index]
+ + + +

+ 3 Configuration +

+
+ + + +

+ 3.1 Autolinking +

+
+ +

To turn off automatically generated links:

+ +
class ApplicationResource < Graphiti::Resource
+  self.autolink = false
+end
+ + + +

+ 3.2 Endpoint Validation +

+
+ +

To turn off Endpoint Validation:

+ +
class ApplicationResource < Graphiti::Resource
+  self.validate_endpoints = false
+end
+ + + +

+ 3.3 Links-on-Demand +

+
+ +

To only render links when requested in the URL with ?links=true:

+ +
Graphiti.configure do |c|
+  c.links_on_demand = true
+end
+ + + +

+ 3.4 Pagination Links +

+
+ +

Requesting big collections can result into slow responses sometimes. In order to avoid this, you could use pagination. It’ll break your response into smaller pieces that will make your server responds faster. Paginations links can be present in your response in the following ways:

+ + + +

+ 3.4.1 Showing by default +

+
+ +

With this configuration, all the responses will return the pagination links

+ +
Graphiti.configure do |c|
+  c.pagination_links = true
+end
+ + + +

+ 3.4.2 When requested +

+
+ +

You can showing the pagination links when it was requested in the URL with ?pagination_links=true

+ +
Graphiti.configure do |c|
+  c.pagination_links_on_demand = true
+end
+ +

Pagination links won’t show up for #show actions.

+ + + +

+ 3.5 Custom Endpoint URLs +

+
+ +

To change the URL associated with a Resource:

+ +
class PostResource < ApplicationResource
+  # Most commonly seen in ApplicationResource
+  self.endpoint_namespace = '/api/v1'
+
+  primary_endpoint '/posts', [:index, :show]
+  # OR
+  self.endpoint[:path] = '/posts'
+  # OR
+  self.endpoint[:actions] = [:index, :show]
+end
+ +

To generate a Relationship Link manually:

+ +
has_many :comments do
+  link do |post|
+    helpers = Rails.application.routes.url_helpers
+    helpers.comments_url(params: { filter: { post_id: post.id } })
+    # or
+    # http://example.com/api/v1/comments?filter[post_id]=123
+  end
+end
+ +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/concepts/remote-resources.html b/website/static/1.13/guides/concepts/remote-resources.html new file mode 100644 index 00000000..695fcf90 --- /dev/null +++ b/website/static/1.13/guides/concepts/remote-resources.html @@ -0,0 +1,536 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+

Remote Resources

+ + +
+ +
+ + + +

+ 1 Overview +

+
+ +

Too often it seems the thinking is, “🤔 We want separate services, +we’ll figure out our API contract along the way”. Graphiti approaches +from the opposite angle - figure out the API contract, and you get separate services as a side-effect 😃💡

+ +

+ +

+ +

Resources have a defined query contract, and connect together with Links. Put two and two together, and you’ll see a Resource doesn’t need to be local to a single application. We can have Remote Resources as well:

+ +
has_many :comments,
+  remote: 'http://blog-api.com/api/v1/comments'
+ +

You might want separate applications that are independently deployable, +or you might want to break apart that slow test suite. The draw of +isolated services is clear. Though you should be aware of the tradeoffs +when breaking apart a Majestic Monolith, +Graphiti helps you lessen those tradeoffs.

+ +

The most common service problem I see is a breakdown in cross-service +communication:

+ +
    +
  • 🚫 No consistent or flexible query interface
  • +
  • 🚫 No consistent error handling
  • +
  • 🚫 No clear patterns on when and why to separate services
  • +
  • 🚫 No types or backwards-compatibility checks (unless GraphQL)
  • +
  • 🚫 A fair amount of glue code required
  • +
+ +

Graphiti was built from the ground up to address all these points. We +have a defined query contract, errors payload, a schema with types and +backwards-compatibility checks, and organize code into RESTful +Resources. Not only can we facilitate cross-service communication, +we can automate it.

+ +
+

Note: Remote Resources are for read operations only. The exception +is associating to an existing belongs_to remote entity.

+
+ +
+

Note: We use Faraday to hit +the remote API. You must add faraday to your Gemfile to enable +remote resources.

+
+ + + +

+ How it Works +

+
+ +

Let’s take a simple association:

+ +
class PostResource < ApplicationResource
+  has_many :comments
+end
+ +

This would generate a Link for +lazy-loading comments:

+ +
{
+  related: "http://my-api.com/api/v1/comments?filter[post_id]=123"
+}
+ +

Critically, those same lazy-loading parameters are used when +eager-loading:

+ +
# under the hood
+posts = PostResource.all.data
+CommentResource.all(filter: { post_id: 123 })
+ +

OK, and we also know Resources support any backend, and we can build an Adapter if our backend supports common operations like filtering, sorting, and pagination.

+ +

So, that means we can build an Adapter that makes an HTTP request to another Graphiti Resource that lives in a separate API. That adapter is built into Graphiti and comes out-of-the-box: Graphiti::Adapters::GraphitiAPI

+ +
class CommentResource < ApplicationResource
+  self.remote = "http://my-api.com/api/v1/comments"
+  # under-the-hood, this sets:
+  # self.adapter = Graphiti::Adapters::GraphitiAPI
+end
+ +

This Resource works as normal. We can execute queries:

+ +
comments = CommentResource.all({
+  sort: '-id',
+  filter: { active: true }
+})
+
+# The model instances are OpenStructs
+comments.data # => [#<OpenStruct>, #<OpenStruct>, ...]
+
+# Those models reflect all the properties returned from the API:
+comments.data.map(&:author) # => ["Jane Doe", "John Doe", ...]
+ +

And we can sideload just like we always do:

+ +
class PostResource < ApplicationResource
+  # Nothing to see here!
+  has_many :comments
+end
+ +

We’ll still support Deep Querying - let’s fetch the Post and its +active comments, ordered by created_at:

+ +

/posts?include=comments&sort=comments.created_at&filter[active]=true

+ +

Let’s say CommentResource has an association to Author. If +AuthorResource is defined in the remote API, we can fetch it as +normal - no special configuration needed to fetch the Post, Comments +and Authors in a single request.

+ +

But maybe only CommentResource is remote, and Authors are local. +We need only define the association locally:

+ +
class CommentResource < ApplicationResource
+  self.remote = "http://my-api.com/api/v1/comments"
+
+  belongs_to :author
+end
+ +

Let’s say we need to tweak the display of a property coming from the +remote API. Again, works just like normal:

+ +
class CommentResource < ApplicationResource
+  self.remote = "http://my-api.com/api/v1/comments"
+
+  attribute :body, :string do
+    @object.body.truncate(100)
+  end
+end
+ +

You only need to define attributes when overriding this logic - +otherwise we’ll take them directly from the API response. This means you +don’t have to update two repos and coordinate deploys - as soon as you +add a property to the remote API and deploy it, it will be reflected in +the local API response.

+ +

For the typical use case, we don’t even need to create this Resource +class. The sideload definition accepts a remote: option, which will +create a Remote Resource under-the-hood:

+ +
class PostResource < ApplicationResource
+  has_many :comments, remote: 'http://my-api.com/api/v1/comments'
+end
+
+# Equivalent to:
+#
+# class PostResource < ApplicationResource
+#   has_many :comments
+# end
+#
+# class CommentResource < ApplicationResource
+#   self.remote = 'http://my-api.com/api/v1/comments'
+# end
+ +
+

NOTE: When sending a request to a remote API, we request page size +999 so results don’t get accidentally cut off. If you need +successive requests, please submit an issue.

+
+ + + +

+ Customizing +

+
+ +

We use Faraday under-the-hood, +which allows for various adapters and middleware. In addition:

+ + + +

+ Configure Timeout +

+
+ +
class CommentResource < ApplicationResource
+  self.remote = "..."
+
+  # Customize faraday timeout
+  self.timeout = 10
+  self.open_timeout = 20
+end
+ + + +

+ Configure Request +

+
+ +
class CommentResource < ApplicationResource
+  self.remote = "..."
+
+  def make_request(url)
+    # request here is from Faraday:
+    #
+    # conn.get do |req|
+    #   yield req
+    # end
+    #
+    super do |request|
+      request.headers["Custom"] = "Header"
+    end
+  end
+end
+ + + +

+ Configure Headers +

+
+ +

By default we’re going to forward the Authorization header of the request to the remote API. To override the default headers sent:

+ +
# app/resources/comment_resource.rb
+def request_headers
+  { "Some-Foo" => "bar" }
+end
+ + + +

+ Error Handling +

+
+ +

If the remote API has an error, we want to re-raise that same error. But +unless you’ve enabled displaying raw errors, we won’t be able to - the only information we have is what’s returned from the API.

+ +

You’re encouraged to display raw errors when an internal or privileged +user:

+ +
rescue_from Exception do |e|
+  handle_exception(e,  show_raw_error: current_user.developer?)
+end
+ +

If you do this, we’ll be able to re-raise the original error, including +stacktrace. If raw errors are not enabled, we’ll raise whatever +information is given.

+ +

Both styles will be wrapped in Graphiti::Errors::Remote, so you can +differentiate between a local error and a remote one.

+ + + +

+ Testing +

+
+ +

When testing a remote resource, we need to mock the API request and +response. Graphiti gives you a spec helper to do just that - +include_context "remote api":

+ +
describe 'comments' do
+  include_context 'remote api'
+
+  let(:api_response) do
+    {
+      data: [{
+        id: '1',
+        type: 'comments',
+        attributes: { body: 'hello' }
+      }]
+    }
+  end
+
+  it 'does something' do
+    url = 'http://my-api.com/api/v1/comments?page[size]=999'
+    mock_api(url, api_response)
+    # ... test ...
+  end
+end
+ +

This shows all the pieces needed to test remote APIs. We want to test

+ +
    +
  • The correct URL is hit
  • +
  • When given a valid response, the rest of the flow works as expected.
  • +
+ +
+

NOTE: if the remote relationship is a has_many, the API will need to +return the foreign key as part of the response. Otherwise, we won’t +know how to associate these children to their parents.

+
+ +

Here’s a slightly longer version, showing that Post can sideload +Comments:

+ +
describe 'sideloading' do
+  describe 'comments' do
+    include_context 'remote api'
+
+    let!(:post) { create(:post) }
+
+    let(:api_response) do
+      {
+        data: [{
+          id: '789',
+          type: 'comments',
+          attributes: { body: 'hello' }
+        }]
+      }
+    end
+
+    before do
+      params[:include] = 'comments'
+    end
+
+    it 'does something' do
+      url = "http://my-api.com/api/v1/comments"
+      url += "?filter[post_id]=#{post_id}"
+      mock_api(url, api_response)
+      render
+      sl = d[0].sideload(:comments)
+      expect(sl.map(&:id)).to eq(['789'])
+      expect(sl.map(&:jsonapi_type).uniq)
+      .to eq(['comments'])
+    end
+  end
+end
+ +
+

Make sure to include page[size]=999 in the test URL!

+
+
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/concepts/resources.html b/website/static/1.13/guides/concepts/resources.html new file mode 100644 index 00000000..811b2fb1 --- /dev/null +++ b/website/static/1.13/guides/concepts/resources.html @@ -0,0 +1,2176 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ + + +

+ 1 Overview +

+
+ +

+ +

+ +

The same way a Model is an abstraction around a database table, a +Resource is an abstraction around an API endpoint. It holds logic for +querying, persisting, and serializing data.

+ +
+

For a condensed view of the Resource interface, see the +cheatsheet.

+
+ + + +

+ 2 Attributes +

+
+ +

A Resource is composed of Attributes. Each Attribute has a +name (e.g. first_name) that corresponds to a JSON key, and a +Type (e.g. string) that corresponds to a JSON value.

+ +

To define an attribute:

+ +
attribute :first_name, :string
+ + + +

+ 2.1 Limiting Behavior +

+
+ +

Each attribute consists of five flags: readable, writable, +sortable, filterable, and schema. Any of these flags can be turned off:

+ +
attribute :name, :string, sortable: false
+ +

Or use only/except shorthand:

+ +
attribute :name, :string, only: [:sortable]
+attribute :name, :string, except: [:writable]
+ +

The schema flag is not affected by only/except options. +This option determines if the attribute is exported to the schema.json.

+ +

You might want to allow behavior only if a certain condition is met. +Pass a symbol to guard this behavior via corresponding method, only allowing the +behavior if the method returns true:

+ +
attribute :name, :string, writable: :admin?
+
+def admin?
+  # ... logic ...
+end
+ +

When guarding the :readable flag, the method can optionally accept the +model instance and the name of the attribute being serialized as arguments:

+ +
attribute :name, :string, readable: :allowed?
+attribute :age, :integer, readable: :attribute_allowed?
+
+def allowed?(model_instance)
+  model_instance.internal == false
+end
+
+def attribute_allowed?(model_instance, attribute_name)
+  PolicyChecker.new(model_instance).attribute_readable?(attribute_name)
+end
+ +

:writable guards mirror this: the guard method (or proc) can optionally accept the model being written and the name of the attribute. On an update, the model is the persisted record being modified; on a create, it is a new unsaved instance. The model is only looked up when a guard actually declares a parameter for it, so zero-argument guards behave exactly as before.

+ +
attribute :salary, :integer, writable: :salary_writable?
+
+def salary_writable?(model_instance, attribute_name)
+  PolicyChecker.new(model_instance).attribute_writable?(attribute_name)
+end
+ +

If the guard returns false, the request is rejected with an unwritable_attribute validation error before anything is persisted.

+ + + +

+ 2.2 Default Behavior +

+
+ +

By default, attributes are enabled for all behavior. You may want to +disable certain behavior globally, for example a read-only API. Use +these properties to affect all subclasses:

+ +
self.attributes_readable_by_default = false # default true
+self.attributes_writable_by_default = false # default true
+self.attributes_filterable_by_default = false # default true
+self.attributes_sortable_by_default = false # default true
+self.attributes_schema_by_default = false # default true
+ +

As for resource defined guards, you can pass a symbol to guard the +behavior globally. This can be used to globally delegate access control to a +dedicated system.

+ +
self.attributes_readable_by_default = :attribute_readable? # default true
+
+def attribute_readable?(model_instance, attribute_name)
+  PolicyChecker.new(model_instance).attribute_readable?(attribute_name)
+end
+ + + +

+ 2.3 Customizing Display +

+
+ +

Pass a block to attribute to customize display:

+ +
attribute :name, :string do
+  @object.name.upcase
+end
+ +

@object will be an instance of your model.

+ + + +

+ 2.4 Types +

+
+ +

Each Attribute has a Type. Each Type defines behavior for

+ +
    +
  • Reading
  • +
  • Writing
  • +
  • Filtering
  • +
+ +

For each of these, we’ll first attempt to coerce the given value to +the correct type. If that fails, we will raise an error.

+ +

The implementation for each of these actions lives in a Dry Type. Take the :integer_id type: here we want to render a string, but query with an integer (this is the default for all Resource id attributes):

+ +
Graphiti::Types[:integer_id]
+
+# {
+#   params: Dry::Types['coercible.integer'],
+#   read: Dry::Types['coercible.string'],
+#   write: Dry::Types['coercible.integer'],
+#   ...
+# }
+ +

You can edit these implementations as you wish. Let’s make the :string type +render an integer:

+ +
Graphiti::Types[:string][:read] = Dry::Types['coercible.integer']
+ +

The built-in Types are:

+ +
    +
  • integer_id
  • +
  • string
  • +
  • integer
  • +
  • big_decimal
  • +
  • float
  • +
  • date
  • +
  • datetime
  • +
  • uuid
  • +
  • string_enum
  • +
  • integer_enum
  • +
  • boolean
  • +
  • hash
  • +
  • array
  • +
+ +

All but the last 3 have Array doppelgängers: array_of_integers, +array_of_dates, etc.

+ +

The integer_id type says, “render as a string, but query as an +integer” and is the default for the id attribute. The uuid type says +“this is a string, but query me case-sensitive by default”.

+ + + +
+ 2.5 Enum Types +
+
+ +

Graphiti provides two built enum types, string_enum and integer_enum. These behave +in exactly the same way as the string and integer types, respectively, except that +when declaring them as either an attribute or a filter you are required to +pass the allow option, which is the list of acceptable values for the field:

+ +
attribute :status, :string_enum, allow: ['draft', 'published']
+ +

Or if your attribute is backed by an ActiveRecord, you could reference +the values directly

+ +
# app/models/post.rb
+class Post < ApplicationRecord
+  enum status: {
+    draft: 0,
+    published: 1
+  }
+end
+
+# app/resources/post_resource.rb
+class PostResource < ApplicationResource
+  attribute :status, :string_enum, allow: Post.statuses.keys
+end
+ +

See the section on filter options for more details on allow behavior

+ +

Note: Graphiti does not currently do any value checking on enum fields when +writing an attribute, and it still expects that your model layer will validate any +data coming in.

+ + + +
+ 2.6 Custom Types +
+
+ +

Dry Types supports custom types. Let’s register a “capital letters” type:

+ +
# Define the Type
+definition = Dry::Types::Nominal.new(String)
+type = definition.constructor do |input|
+  input.upcase
+end
+
+# Register it with Graphiti
+Graphiti::Types[:caps_lock] = {
+  params: type,
+  read: type,
+  write: type,
+  kind: 'scalar',
+  canonical_name: :caps_lock,
+  description: 'All capital letters'
+}
+
+# Use in a Resource
+attribute :name, :caps_lock
+ + + +

+ 3 Querying +

+
+ +

Resources must be able to dynamically compose a query that can be run +against an arbitrary backend (SQL, NoSQL, service calls, etc). They do +this through the concept of scoping.

+ +

The best way to understand scoping is to take a look at what happens “under the hood”. Here’s the simple Resource, where most of the logic is hiding in the Adapter:

+ +
class PostResource < ApplicationResource
+  attribute :title, :string
+end
+ +

Now let’s show the long-hand version. This is completely runnable code (we’re just overriding the default behavior with an explicit version of the same):

+ +
class PostResource < ApplicationResource
+  filter :title do |scope, value|
+    eq do |scope, value|
+      scope.where(title: value)
+    end
+  end
+
+  sort :title do |scope, dir|
+    scope.order(title: dir)
+  end
+
+  paginate do |scope, current_page, per_page|
+    scope.page(current_page).per(per_page)
+  end
+
+  def base_scope
+    Post.all
+  end
+
+  def resolve(scope)
+    scope.to_a
+  end
+end
+ +

Let’s break this down the key elements:

+ +
def base_scope
+  Post.all
+end
+ +

Graphiti builds queries just like ActiveRecord: start with a base scope (Post.all), and alter that scope based on the incoming request. #base_scope defines our starting point.

+ +
filter :title do |scope, value|
+  eq do |scope, value|
+    scope.where(title: value)
+  end
+end
+ +

When the title query parameter is present, we alter the scope.

+ +
def resolve(scope)
+  scope.to_a
+end
+ +

The #resolve method is in charge of actually executing the query +and returning model instances.

+ +

In other words, this code is roughly equivalent to:

+ +
scope = Post.all # #base_scope
+if value = params[:filter].try(:[], :title)
+  scope = scope.where(title: value) # .filter
+end
+scope.to_a # #resolve
+ + + +

+ 3.1 Query Interface +

+
+ +

Resources can query and persist data without an API request or +response. To query, pass a JSONAPI-compliant query hash:

+ +
EmployeeResource.all({
+  filter: { first_name: 'Jane' },
+  sort: '-created_at',
+  page: { size: 10, number: 2 }
+})
+ +

The return value from .all is a proxy object, similar to +ActiveRecord::Relation:

+ +
# ActiveRecord:
+employees = Employee.all
+employees.class # ActiveRecord::Relation
+# No query fires until .map
+employees.map(&:first_name) # => ["Jane", "Joe", ...]
+
+# Graphiti Resource:
+employees = EmployeeResource.all
+employees.class # Graphiti::ResourceProxy
+# No query fires until .map
+employees.map(&:first_name) # => ["Jane", "Joe", ...]
+
+# Access model instances directly
+employees.data # => [#<Employee>, #<Employee>, ...]
+ +

This proxy object can render JSONAPI, simple +JSON, or XML:

+ +
employees = EmployeeResource.all
+employees.to_jsonapi
+employees.to_json
+employees.to_xml
+ +

Use .find to find a single record by id, raising +Graphiti::Errors::RecordNotFound if no records are returned:

+ +
employee = EmployeeResource.find(id: 123)
+employee.data.first_name # => "Jane"
+ +

Note: SomeResource.find returns a ResourceProxy. To access the model/record proper you will want to make sure you call .data on the result of find as shown above.

+ + + +

+ 3.2 Composing with Scopes +

+
+ + + +

+ 3.2.1 #base_scope +

+
+ +

Override the #base_scope method whenever you have logic that should +apply to every query. For example, if we only ever wanted to return +active Positions:

+ +
def base_scope
+  Position.where(active: true)
+end
+ +

This can be overridden by passing a second argument to Resource.all:

+ +
class InactivePostsController < PostsController
+  def index
+    posts = PostResource.all(params, Post.where(active: false))
+    respond_with(posts)
+  end
+end
+ + + +

+ 3.4 Sort +

+
+ +

Use the sort DSL to customize sorting behavior.

+ +
sort :name, :string do |scope, direction|
+  scope.order(first_name: direction, last_name: direction)
+end
+ +

If you’ve already defined a corresponding attribute, you’ll be +overriding that default behavior (and there is no need to pass a type as +the second argument):

+ +
attribute :name, :string
+
+sort :name do |scope, direction|
+  # ... code ...
+end
+ +
+

Note: sort defines a sort-only attribute. If you want other +behavior, like filtering, it’s best to define the attribute first.

+
+ + + +
+ 3.4.1 Sort Options +
+
+ +

Pass :only if you support just a single direction:

+ +
sort :name, only: [:desc]
+ + + +

+ 3.5 Filter +

+
+ +

Use the filter DSL to customize each operator:

+ +
filter :name, :string do
+  eq do |scope, value|
+    scope.where(first_name: value)
+  end
+
+  # prefix do ... end
+  # suffix do ... end
+  # etc
+end
+ +

The built-in operators for ActiveRecord are:

+ +
    +
  • eq (case-insensitive)
  • +
  • eql (case-sensitive)
  • +
  • prefix
  • +
  • suffix
  • +
  • match
  • +
  • gt (greater-than)
  • +
  • gte (greater-than-or-equal-to)
  • +
  • lt (less-than)
  • +
  • lte (less-than-or-equal-to)
  • +
+ +
+

Note that Graphiti expects filters to support multiple values by +default, so value will be an array. Pass single: true if you do +not support multiple values.

+
+ +
+

To pass multiple values in a query string, comma-delimit: +/employees?filter[name]=Jane,John

+
+ +

If you’ve already defined a corresponding attribute, you’ll be +overriding that default behavior (and there is no need to pass a type as +the second argument):

+ +
attribute :name, :string
+
+filter :name do
+  eq do |scope, value|
+    # ... code ...
+  end
+end
+ +

You can define custom operators on-the-fly:

+ +
filter :name do
+  fuzzy_match do |scope, value|
+    # ... code ...
+  end
+end
+ +

Will now support filter[name][fuzzy_match]=foo

+ +
+

Note: filter defines a filter-only attribute. If you want other +behavior, like sorting, it’s best to define the attribute first.

+
+ + + +
+ 3.5.1 Filter Options +
+
+ +

Pass :only or :except to limit possible operators:

+ +
filter :name, :string, only: [:eq, :suffix]
+ +

Pass :allow or :reject to only allow filtering on certain values, or +reject bad values:

+ +
filter :size, :string, allow: ['Big', 'Medium', 'Small']
+
+filter :size, :string, reject: ['X-Large']
+ +

By default, all filters accept multiple values, causing the yielded +value to always be an array. Pass single: true to only allow a +single value:

+ +
# Default behavior
+filter :name, :string do
+  eq do |scope, value|
+    value # => ["Jane"]
+  end
+end
+
+# With single: true
+filter :name, :string, single: true do
+  eq do |scope, value|
+    value # => "Jane"
+  end
+end
+ +

Filters can be required:

+ +
# Via attribute
+attribute :customer_id, :integer, filterable: :required
+
+# Via filter
+filter :customer_id, :string, required: true
+ +

Filters can also depend on other filters, requiring all criteria to be +present:

+ +
# We query customers by id AND type, not one or the other
+filter :customer_id, :integer, dependent: [:customer_type]
+filter :customer_type, :string, dependent: [:customer_id]
+ + + +
+ 3.5.2 Boolean Filter +
+
+ +

It doesn’t make sense for a filter with type boolean to accept +multiple values. These filters will be single: true by default.

+ + + +
+ 3.5.3 Hash Filter +
+
+ +

Filters with type hash will automatically parse JSON when passed in a +URL query string:

+ +
# GET /employees?filter[metadata]={ "foo": 100 }
+
+filter :metadata, :hash do
+  eq do |scope, value|
+    value # => [{ "foo" => 100 }]
+  end
+end
+ + + +
+ 3.5.4 Escaping Values +
+
+ +

By default, Graphiti parses a comma-delimited string as an array. There +are times you may not want this - for instance a “keyword search” field +that could contain a comma.

+ +

Wrap values in {{curlies}} to avoid parsing:

+ +
# GET /employees?filter[keywords]={{some,value}}
+
+filter :keywords, :string do
+  eq do |scope, value|
+    value # => "some,value"
+  end
+end
+ +

You can also define arrays explicitly instead of delimiting on comma:

+ +
# GET /employees?filter[keywords]=[some,value]
+
+filter :keywords, :string do
+  eq do |scope, value|
+    value # => ["some", "value"]
+  end
+end
+ +

If a filter is marked single: true, we’ll avoid any array parsing and +escape the value for you, filtering on the string as given.

+ +

By default a value that comes in as null is treated as a string "null". +To coerce null to a Ruby nil mark the filter with allow_nil: true. +This can be changed for all attributes by setting filters_accept_nil_by_default

+ +
class PostResource < ApplicationResource
+  self.filters_accept_nil_by_default = true
+end
+ + + +

+ 3.6 Statistics +

+
+ +

Statistics are useful and common. Consider a datagrid listing posts - we might want a “Total Posts” count displayed above the grid without firing an additional request. Notably, that statistic should take into account filtering, but should not take into account pagination.

+ +

All resources have a total count statistic by default:

+ +
PostResource.all({
+  stats: { total: 'count' }
+})
+ +

/posts?stats[total]=count

+ +

Would cause the meta section of the response to be:

+ +
{
+  meta: {
+    stats: {
+      total: {
+        count: 100
+      }
+    }
+  }
+}
+ +

Allow a given statistic to be requested using .stat:

+ +
stat total: [:count]
+stat rating: [:average]
+stat likes: [:sum]
+stat score: [:maximum]
+stat score: [:maximum]
+
+# e.g.
+# {
+#   meta: {
+#     stats: {
+#       rating: {
+#         average: 74
+#       }
+#     }
+#   }
+# }
+ +

You can also define custom statistics:

+ +
stat rating: [:average] do
+  standard_deviation do |scope, attr|
+    # your standard deviation code here
+  end
+end
+ + + +

+ 3.7 Extra Fields +

+
+ +

Sometimes you have a field that is not always needed, and perhaps +computationally expensive. In this case, you only want the field +returned when explicitly requested by the client. To do this:

+ +
extra_attribute :net_worth
+ +

This works just like attribute, except the field is read-only and will +only be returned when requested. The query parameter signature matches +fields: ?extra_fields[employees]=net_worth.

+ +

You may want to adjust your scope to eager load data when a given extra +field is requested. To do this:

+ +
resource.on_extra_attribute :net_worth do |scope|
+  scope.includes(:assets)
+end
+ + + +

+ 3.8 #resolve +

+
+ +

After we build up a query, we pass it to #resolve. Resolve must do +two things:

+ +
    +
  • Execute the query
  • +
  • Return an array of Model instances
  • +
+ +

Override #resolve if you need more than the default behavior:

+ +
def resolve(scope)
+  Rails.logger.info "begin resolving scope..."
+  result = super
+  Rails.logger.info "resolved!"
+  result
+end
+ + + +

+ 4 Configuration +

+
+ +

Here’s a Resource with explicit defaults:

+ +
class PostResource < ApplicationResource
+  self.model = Post
+  self.type = 'posts'
+
+  # Only used if you care about Links
+  primary_endpoint '/posts', [:index, :show, :create, :update, :destroy]
+
+  # default nil
+  self.default_sort = [{ title: :asc }]
+
+  # default 20
+  self.default_page_size = 10
+end
+ +

Typically you’d inherit from ApplicationResource. Here are some common higher-level customization options that will affect subclasses:

+ +
class ApplicationResource < Graphiti::Resource
+  # Must be set when no corresponding model/query
+  self.abstract_class = true
+
+  # Subclasses can override if needed
+  self.adapter = Graphiti::Adapters::ActiveRecord
+
+  # Default attribute flags:
+  # attribute :title, :string,
+  #   readable: default,
+  #   writable: default,
+  #   sortable: default,
+  #   filterable: default
+  self.attributes_readable_by_default = true
+  self.attributes_writable_by_default = true
+  self.attributes_sortable_by_default = true
+  self.attributes_filterable_by_default = true
+
+  # Used for link generation
+  self.base_url = Rails.application.routes.default_url_options[:host]
+  # Used for link generation
+  # Suggest referencing this config/routes.rb:
+  # scope path: ApplicationResource.endpoint_namespace do
+  #   resources :posts
+  # end
+  self.endpoint_namespace = '/api/v1'
+
+  # Will raise an error if a resource is being accessed from a URL it is not allowlisted for
+  # Helpful for link validation
+  self.validate_endpoints = false
+
+  # Automatically generate JSONAPI links?
+  self.autolink = true
+end
+ + + +

+ 4.1 Polymorphic Resources +

+
+ +

Polymorphic Resources are similar to ActiveRecord STI: when a single query can return multiple Resource instances. We may query /tasks, but return bugs, features, epics, etc.

+ +

For example, given the ActiveRecord models:

+ +
class Employee < ApplicationRecord
+  has_many :tasks
+end
+
+# tasks table has a 'type' column
+class Task < ApplicationRecord
+  belongs_to :employee
+end
+
+class Bug < Task
+end
+
+# ONLY Feature has #points
+class Feature < Task
+  def points
+    5
+  end
+end
+
+# ONLY Epic has the milestones relationship
+class Epic < Task
+  has_many :milestones
+end
+
+class Milestone < ApplicationRecord
+  belongs_to :epic
+end
+ +

We could define the following Polymorphic Resources:

+ +
class TaskResource < ApplicationResource
+  # Reference child classes
+  self.polymorphic = [
+    'BugResource',
+    'FeatureResource',
+    'EpicResource'
+  ]
+
+  attribute :title, :string
+end
+
+class BugResource < TaskResource
+end
+
+class FeatureResource < TaskResource
+  attribute :points, :integer
+end
+
+class EpicResource < TaskResource
+  has_many :milestones
+end
+
+class MilestoneResource < TaskResource
+  belongs_to :epic
+end
+ +

If we hit a /tasks endpoint, we’d get back JSONAPI types of bugs, features and epics. Only features would render the points attribute, and only epics would render the milestones relationship.

+ +

A query to /tasks?include=milestones would correctly only query +and render Milestones for Epics.

+ + + +

+ 5 Relationships +

+
+ +

Resources can connect to other Resources via relationships. +Each relationship determines behavior for:

+ +
    +
  • Sideloading (load both Resources in a single request)
  • +
  • Links (URL to lazy-load in separate request)
  • +
  • Sideposting (save both in single request)
  • +
+ +

When connecting resources, you can imagine the logic similar to +ActiveRecord’s .includes:

+ +
class PostResource < ApplicationResource
+  has_many :comments
+end
+
+class CommentResource < ApplicationResource
+  attribute :post_id, :integer, only: [:filterable]
+  belongs_to :post
+end
+
+PostResource.all(include: 'comments')
+# Under the hood:
+# CommentResource.all(filter: { post_id: array_of_post_ids })
+
+CommentResource.all(include: 'post')
+# Under the hood:
+# PostResource.all(filter: { id: array_of_comment_ids })
+ +
+

Note the explicit post_id filter on CommentResource

+
+ + + +

+ 5.1 Deep Queries +

+
+ +

A query that applies to a relationship is referred to as a deep +query. Use the dot-syntax to deep query:

+ +

/employees?include=positions&filter[positions.title]=Manager

+ +

/employees?include=positions.department&filter[positions.department.name]=Engineering

+ +

The above references the relationship name. For simplicity, you can +also pass the JSONAPI type in brackets:

+ +

/employees?include=positions.department&filter[departments][name]=Engineering

+ +

Sorting and pagination currently only support the JSONAPI type:

+ +

/employees?include=positions.department&sort=departments.name

+ +

/employees?include=positions.department&page[departments][size]=10

+ + + +

+ 5.2 Customizing Relationships +

+
+ +

The default options you can override are:

+ +
has_many :positions,
+  foreign_key: :employee_id,
+  primary_key: :id,
+  resource: EmployeeResource,
+  readable: true,
+  writable: true,
+  link: self.autolink, # default true
+  single: false, # only allow this sideload when one employee
+  always_include_resource_ids: false
+ +

note: Setting always_include_resource_ids: true could result in 1+N queries (see #167)

+ + + +
+ 5.2.1 Conditional Relationships +
+
+ +

Like attributes, the readable and writable flags on a relationship accept more than a boolean: pass a symbol, string, or proc and the relationship becomes conditional, evaluated per-request.

+ +
class EmployeeResource < ApplicationResource
+  has_many :salary_histories, readable: :admin?, writable: :admin?
+
+  def admin?
+    context.current_user.admin?
+  end
+end
+ +

When a readable guard returns false, the relationship is omitted from the serialized output and any attempt to sideload it via ?include= is silently scrubbed from the request. When a writable guard returns false, sideposting to that relationship is rejected with an unwritable_relationship validation error.

+ +

Unlike attribute guards, relationship guards take no arguments. Include scrubbing happens before any records have been fetched, so there is no model to hand them — base the decision on context alone.

+ +

The guard can live on either side of the relationship. Graphiti first looks for the method on the resource declaring the relationship; if it isn’t defined there but is defined on the related resource, the related resource’s method is used. Defining the guard on the related resource lets a single guard cover every relationship pointing at it:

+ +
class SalaryHistoryResource < ApplicationResource
+  # Any resource declaring a relationship to SalaryHistoryResource with
+  # readable: :admin? will use this method, unless it defines its own.
+  def admin?
+    context.current_user.admin?
+  end
+end
+ +
+

Upgrading to 1.12: relationship guards are new enforcement, not a new +option. Before 1.12, a symbol, string, or proc passed to a relationship’s +readable/writable was accepted and silently treated as true — the guard +was never called. Those guards now run. If your app already passes one of +these, a relationship that has been serialized all along may start +disappearing from responses.

+ +

To list every guarded relationship in your app before deploying, run +bin/rails runner 'puts Graphiti.guarded_relationships'.

+ +

Apps using schema.json also get this for free: guarded relationships are +flagged in the schema, and the schema check reports them as +became guarded.

+
+ + + +
+ 5.2.2 Customizing Scope +
+
+ +

Use params to change the query parameters that will be passed to the +associated Resource:

+ +
has_many :active_positions, resource: PositionResource do
+  params do |hash, employees|
+    hash[:filter][:active] = true
+  end
+end
+
+# Would cause the underlying query:
+#
+# PositionResource.all({
+#   filter: {
+#     employee_id: array_of_employee_ids
+#     active: true
+#   }
+# })
+ +

If there is no existing AR association for this we would also need to make it a getter/setter on the model.

+ +
# app/models/position.rb
+attr_accessor :active_positions
+ + + +
+ 5.2.3 Customizing Assignment +
+
+ +

Once we’ve fetched primary data and its relationship (e.g. we have an +employees array and positions array), we need to associate these +objects:

+ +
employees.each do |e|
+  e.positions = positions.select { |p| p.employee_id == e.id }
+end
+ +

Occasionally this logic will be non-standard or more complex. Use +assign_each to customize, returning all relevant children for the +given parent:

+ +
has_many :positions do
+  assign_each do |employee, positions|
+    positions.select { |p| p.belongs_to?(employee) }
+  end
+end
+ +

Or if all else fails, use #assign to control all the logic:

+ +
has_many :positions do
+  assign do |employees, positions|
+    employees.each do |employee|
+      positions.select { |p| p.belongs_to?(employee) }
+    end
+  end
+end
+ +

Note: ActiveRecord will sometimes cause unexpected queries when +assigning. If you’re overriding #assign, make sure to keep an eye on +this. If using #assign_each, you’re fine because the adapter will take +care of this for you.

+ + + +

+ 5.3 has_many +

+
+ +
has_many :positions
+ +

Defaults to these common options:

+ +
has_many :positions,
+  foreign_key: :employee_id,
+  primary_key: :id,
+  always_include_resource_ids: false,
+  resource: PositionResource
+ +

Which would cause the following query when sideloading:

+ +
PositionResource.all({ filter: { employee_id => employee_ids } })
+ +

This means we need to make sure that filter is supported:

+ +
class PositionResource < ApplicationResource
+  attribute :employee_id, :integer, only: [:filterable]
+  # ... code ...
+end
+ +

Once we’ve resolved employees and positions the resulting objects +would be associated with logic similar to:

+ +
employees.each do |e|
+  e.positions = positions.select { |p| p.employee_id == e.id }
+end
+ +

And generate a Link:

+ +

/positions?filter[employee_id]=1,2,3

+ + + +

+ 5.4 belongs_to +

+
+ +
belongs_to :employee
+ +

Defaults to these common options:

+ +
belongs_to :employee,
+  foreign_key: :employee_id,
+  primary_key: :id,
+  always_include_resource_ids: false,
+  resource: EmployeeResource
+ +

Which would cause the following query when sideloading:

+ +
EmployeeResource.all({ filter: { id => position_ids } })
+ +

And assign the resulting objects with logic similar to:

+ +
positions.each do |p|
+  p.employee = employees.find { |e| p.employee_id == e.id }
+end
+ +

And generate a Link:

+ +

/employees?filter[id]=1,2,3

+ + + +

+ 5.5 has_one +

+
+ +

has_one works exactly like has_many, but only one record will be +returned. When sideloading this will be a single element, much like +belongs_to.

+ +

There is one small caveat: Links always point to an index action, so +we can apply filters. That means following has_one Link will lead to +an array, and you should select the first record.

+ + + +
+ 5.5.1 Faux has_one +
+
+ +

A “Faux Has One” occurs when there is more than one record of +associated data, but we only want to return the first record in that +array. Consider this ActiveRecord relationship:

+ +
# app/models/employee.rb
+has_many :positions
+has_one :current_position, -> { where(created_at: :desc) }, class_name: 'Position'
+
+Employee.includes('current_position').to_a
+
+# SELECT * FROM employees
+# SELECT * FROM positions WHERE employee_id IN (?) ORDER BY created_at DESC
+ +

When we eager load, more than one Position is returned from the +database query. Assigning only the first record and dropping the rest +occurs in ruby, not the database query.

+ +

The same thing happens in Graphiti:

+ +
# app/resources/employee_resource.rb
+has_many :positions
+has_one :current_position, resource: PositionResource do
+  params do |hash|
+    hash[:sort] = '-created_at'
+  end
+end
+
+EmployeeResource.all(include: 'current_position')
+# PositionResource.all({
+#   filter: { employee_id: employee_ids },
+#   sort: '-created_at'
+# })
+ +

Though everything works as expected, a large number of Position records +can incur a performance penalty (as we’d be instantiating a large number +of ActiveRecord objects).

+ +

For this reason, you are encouraged to model Faux Has One’s in such a +way that the underlying database query only returns the relevant single +record. Imagine if we had a historical_index column on positions, +where a value of 1 meant “most recent”:

+ +
# app/models/employee.rb
+has_many :positions
+has_one :current_position, -> { where(historical_index: 1) }, class_name: 'Position'
+
+Employee.includes('current_position').to_a
+
+# SELECT * FROM employees
+# SELECT * FROM positions WHERE employee_id IN (?) AND historical_index = 1
+ +

We’ve ensured the query itself only returns a single record. +Optimizing a Graphiti API is the same as optimizing queries.

+ + + +

+ 5.6 many_to_many +

+
+ +
+

This relationship is specific to relational databases that use a “join +table” between two tables.

+
+ +

Though you can make this work for other ORMs/clients, it’s easiest to +explain by focusing on ActiveRecord.

+ +

First, you must use has_many :through and not has_and_belongs_to_many:

+ +
class Employee < ApplicationRecord
+  has_many :team_memberships
+  has_many :teams, through: :team_memberships
+end
+
+class TeamMembership < ApplicationRecord
+  belongs_to :employee
+  belongs_to :team
+end
+
+class Team < ApplicationRecord
+  has_many :team_memberships
+  has_many :employees, through: :team_memberships
+end
+ +

You can always expose team_memberships to your API - particularly +useful if that table holds metadata about the relationship.

+ +

Other times, however, clients of the API should not have knowledge of +this implementation detail. In these cases, use many_to_many:

+ +
class EmployeeResource < ApplicationResource
+  many_to_many :teams
+end
+# Generates the Link
+# /teams?filter[employee_id]=1,2,3
+
+class TeamResource < ApplicationResource
+  many_to_many :employees
+end
+# Generates the Link
+# /teams?filter[team_id]=1,2,3
+ +

The many_to_many call will automatically add a Filter to the +associated resource. The logic for that filter, in the case of ActiveRecord:

+ +
# app/resources/employee_resource.rb
+
+filter :team_id, :integer do
+  eq do |scope, value|
+    scope
+      .includes(:team_memberships)
+      .where(team_memberships: { team_id: value }
+  end
+end
+ +

To customize the foreign key, you will need to specify a hash rather +than a symbol. The hash key is the relationship name, so the above is +equivalent to

+ +
# app/resources/employee_resource.rb
+
+many_to_many :teams, foreign_key: { team_memberships: :team_id }
+ +

If using ActiveRecord, and the API relationship name does not match your +Model relationship name, use :as to specify the model relationship +that should be used to derive the query:

+ +
# The API relationship is "teams", ActiveRecord has "groups"
+many_to_many :teams, as: :groups
+ + + +

+ 5.7 polymorphic_belongs_to +

+
+ +

With polymorphic associations, a Resource can belong to more than one other Resource, on a single association. Though these relationships are not specific to ActiveRecord, we’ll use ActiveRecord conventions to describe the use case.

+ +

Given the following polymorphic ActiveRecords:

+ +
class Note < ApplicationRecord
+  belongs_to :notable, polymorphic: true
+end
+
+class Employee < ApplicationRecord
+  has_many :notes, as: :notable
+end
+
+class Department < ApplicationRecord
+  has_many :notes, as: :notable
+end
+
+class Team < ApplicationRecord
+  has_many :notes, as: :notable
+end
+ +

By ActiveRecord convention, the notes table would have columns +notable_id and notable_type.

+ +

Graphiti has the same concept. In this case we would group all the notes +by a given notable_type, and follow a different belongs_to +association for each group:

+ +
# app/resources/note_resource.rb
+polymorphic_belongs_to :notable do
+  group_by(:notable_type) do
+    on(:Employee)
+    on(:Department)
+    on(:Team)
+  end
+end
+ +

The on DSL is shorthand for a belongs_to relationship that accepts +all the usual options and customizations:

+ +
on(:Employee).belongs_to :employee,
+  resource: EmployeeResource
+  # ... etc ...
+ +

In other words: group all Notes by notable_type, and for all that have +the value of "Employee" use the belongs_to :employee relationship +for further querying.

+ + + +

+ 5.8 polymorphic_has_many +

+
+ +

Continuing from the prior section, the corresponding association of a +polymorphic_belongs_to is a polymorphic_has_many:

+ +
class EmployeeResource < ApplicationResource
+  polymorphic_has_many :notes, as: :notable
+end
+ +

Predictably, this causes the query:

+ +
NoteResource.all({
+  filter: {
+    notable_type: 'Employee',
+    notable_id: employee_ids
+  }
+})
+ +

And the Link

+ +

/notes?filter[notable_id]=1,2,3&filter[notable_type]=Employee

+ +

Which means the following filters are required:

+ +
class NoteResource < ApplicationResource
+  attribute :notable_id, :integer, only: [:filterable]
+  attribute :notable_type, :string, only: [:filterable]
+  # ... code ...
+end
+ + + +

+ 6 Generators +

+
+ +

To generate a Resource:

+ +
$ rails generate graphiti:resource NAME [attribute:type] [options]
+ +

For example:

+ +
$ rails generate graphiti:resource Employee first_name:string age:integer
+ +

Will add a route, controller, resource, and tests.

+ +

Limit the actions this resource supports with -a:

+ +
$ rails generate graphiti:resource Employee -a index show
+ + + +

+ 7 Persisting +

+
+ +

Graphiti allows writing a graph of data in a single request. We’ll do +the work of parsing the graph and ordering operations, so you can focus +on the part you care about: the logic for actually persisting an object.

+ +

By default, persistence operations are handled by your adapter. The +“expanded” view of the ActiveRecord implementation is below:

+ +
# app/resources/employee_resource.rb
+
+def create(attributes)
+  employee = Employee.new
+  attributes.each_pair do |key, value|
+    employee.send(:"#{key}=", value)
+  end
+  employee.save
+  employee
+end
+
+def update(attributes)
+  employee = EmployeeResource.find(attributes.delete(:id)).data
+  attributes.each_pair do |key, value|
+    employee.send(:"#{key}=", value)
+  end
+  employee.save
+  employee
+end
+
+def destroy(attributes)
+  employee = EmployeeResource.find(attributes.delete(:id)).data
+  employee.destroy
+  employee
+end
+ +
    +
  • You are encouraged not to override these directly. Instead, use +hooks (see next section).
  • +
  • We’ll process any writable: false or guarded attributes prior to +these methods.
  • +
  • After these methods, we’ll check the Model instance for validation +errors, rolling back the transaction if any Model in the graph is +invalid.
  • +
  • These methods must return the Model instance.
  • +
+ + + +

+ 7.1 Persistence Lifecycle Hooks +

+
+ +

Let’s dive into a persistence request. If you look at the code snippets in +the prior section, the flow breaks down into 3 steps:

+ +
    +
  • Build or find the model
  • +
  • Assign attributes to the model
  • +
  • Save
  • +
+ +

You can hook into each step:

+ +
class PostResource < ApplicationResource
+  before_attributes do |attributes|
+    # Before attributes have been assigned to the model
+  end
+
+  after_attributes do |model|
+    # After attributes have been assigned to the model
+  end
+
+  around_attributes :do_around_attributes
+
+  def do_around_attributes(attributes)
+    # before
+    model_instance = yield attributes
+    # after
+  end
+
+  before_save do |model|
+    # After attributes assigned, but before persisting
+  end
+
+  after_save do |model|
+    # After model has been saved
+  end
+
+  around_save :do_around_save
+
+  def do_around_save(model)
+    # before
+    yield model
+    # after
+  end
+
+  # This is an *override*
+  # During #create, build a blank model instance
+  # By default, we'd call adapter.build(model_class)
+  def build(model_class)
+    model_class.new
+  end
+
+  # This is an *override*
+  # During #create/#update, assign new attributes to the model instance
+  # By default, we'd call adapter.assign_attributes(model_instance, attributes)
+  def assign_attributes(model_instance, attributes)
+    attributes.each_pair do |key, value|
+      model_instance.send(:"#{key}=", value)
+    end
+  end
+
+  # This is an *override*
+  # During #create/#update, actually save the model instance
+  # By default, we'd call adapter.save(model_instance)
+  def save(model_instance)
+    model_instance.save
+    model_instance
+  end
+
+
+  # This is an *override*
+  # During #destroy, actually save the model instance
+  # By default, we'd call adapter.destroy(model_instance)
+  def delete(model_instance)
+    model_instance.destroy
+    model_instance
+  end
+
+  # Finally, you may want to hook around *all* the above steps:
+  # Only applies to #create/#update
+  around_persistence :do_around_persistence
+
+  def do_around_persistence(attributes)
+    attributes[:foo] = 'bar'
+    model = yield # build/find, assign attrs, save
+    model.update_counter_cache
+  end
+end
+ +
    +
  • All hooks have only/except options, e.g. before_attributes only: +[:update]
  • +
  • Most hooks can be called with an in-line block, or by passing a method +name (e.g. before_attributes :do_something). The exception is +around_* hooks, which must be called with a method name.
  • +
+ +

When persisting multiple objects at once, we’ll open a database +transaction, process each model individually, ensure all models pass +validation, then close the transaction. This means that if you raise an +error at any point, or any model does not pass validations, the +transaction will be rolled back.

+ +

You may want to perform an operation after all models have been +processed and validated, but before the transaction is closed. One +example is sending an email - you don’t want to send if the models were +invalid, so after_save wouldn’t work. And you still want to do it +within the transaction, so if your email server is down and an error +is raised the transaction gets rolled back.

+ +

For this scenario, use before_commit:

+ +
before_commit do |model|
+  PostMailer.with(post: model).some_email.deliver
+end
+ + + +

+ 7.2 Sideposting +

+
+ +

The act of persisting multiple Resources in a single request is called +Sideposting. The payload mirrors the sideloading payload for +read operations, with minor additions.

+ +

Let’s create a Post and associate it to an existing Blog in a single +request:

+ +
# POST /api/v1/posts
+{
+  type: 'posts',
+  attributes: { title: 'My post' },
+  relationships: {
+    blog: {
+      data: {
+        id: '1',
+        type: 'blogs',
+        method: 'update'
+      }
+    }
+  }
+}
+ +

The critical addition here is the method key. When we persist RESTful +Resources, we send a corresponding HTTP verb. This follows the same +pattern, adding a verb for each Resource in the graph. method can be +one of:

+ +
    +
  • create
  • +
  • update
  • +
  • destroy
  • +
  • disassociate (e.g. null foreign key)
  • +
+ +

When we sidepost, all objects will be persisted within the same database +transaction, which rolls back if an error is raised or any objects are invalid.

+ + + +

+ 7.2.1 Create +

+
+ +

Let’s say we want to create a Post and its Blog in a single request. +You’ll note that we don’t have the id key to generate a Resource +Identifier (combination of id and type +that uniquely identifies a Resource).

+ +

To accomodate this, send an ephemeral temp-id (any UUID):

+ +
{
+  # POST /api/v1/posts
+  {
+    type: 'posts',
+    attributes: { title: 'My post' },
+    relationships: {
+      blog: {
+        data: {
+          :'temp-id' => 'abc123',
+          type: 'blogs',
+          method: 'create'
+        }
+      }
+    },
+    included: [
+      {
+        :'temp-id' => 'abc123'
+        type: 'blogs',
+        attributes: { name: 'New Blog' }
+      }
+    ]
+  }
+}
+ +

This random UUID:

+ +
    +
  • Connects relevant sections of the payload.
  • +
  • Tells clients how to associate their in-memory objects with the ids returned from the server.
  • +
+ + + +

+ 7.2.2 Expanded Example +

+
+ +

Here we’re updating a Post, changing the name of its associated Blog, creating a Tag, deleting one Comment, and disassociating (null foreign key) a different Comment, all in a single request:

+ +
{
+  data: {
+    type: 'posts',
+    id: 123,
+    attributes: { title: 'Updated!' },
+    relationships: {
+      blog: {
+        data: {
+          type: 'blogs',
+          id: 123,
+          method: 'update'
+        }
+      },
+      tags: {
+        data: [{
+          type: 'tags',
+          temp-id: 's0m3uu1d',
+          method: 'create'
+        }]
+      },
+      comments: {
+        data: [
+          {
+            type: 'comments',
+            id: '123',
+            method: 'destroy'
+          },
+          {
+            type: 'comments',
+            id: '456',
+            method: 'disassociate'
+          }
+        ]
+      }
+    }
+  },
+  included: [
+    {
+      type: 'tags',
+      :'temp-id' => 's0m3uu1d',
+      attributes: { name: 'Important' }
+    },
+    {
+      type: 'blogs',
+      id: => '123',
+      attributes: { name: 'Updated!' }
+    }
+  ]
+}
+ + + +

+ 7.3 Validation Errors +

+
+ +

When a persistence operation is attempted but the corresponding Resource +is invalid, the transaction will be rolled back and an errors payload will be returned +with a 422 response code:

+ +
{
+  errors: [{
+    code:  'unprocessable_entity',
+    status: '422',
+    title: "Validation Error",
+    detail: "Title can't be blank",
+    source: { pointer: '/data/attributes/title' },
+    meta: {
+      attribute: :title,
+      message: "can't be blank",
+      code: :blank
+    }
+  }]
+}
+ +

To get this functionality, your Model must adhere to the +ActiveModel::Validations API.

+ +

You get this for free with ActiveRecord, or it can be mixed in to any +PORO:

+ +
class Post
+  include ActiveModel::Validations
+  validates :title, presence: true
+end
+ +

Errors on associations will have a slightly expanded payload:

+ +
{
+  errors: [{
+    code: 'unprocessable_entity',
+    status: '422',
+    title: 'Validation Error',
+    detail: "Name can't be blank",
+    source: { pointer: '/data/attributes/name' },
+    meta: {
+      relationship: {
+        attribute: :name,
+        message: "can't be blank",
+        code: :blank,
+        name: :pets,
+        id: '444',
+        type: 'pets'
+      }
+    }
+  }]
+}
+ +

When Sideposting, the errors payload will contain all +invalid Resources in the graph.

+ + + +

+ 7.4 Read on Write +

+
+ +

By default, the response of a persistence operation will mirror your +request. But sometimes you need control over the response. The most +common scenario is sideloading an additional entity - imagine creating +an order, and wanting the order’s shipping information to come back in +the response.

+ +

You can do this by POSTing the payload as normal, but adding query +parameters to the URL:

+ +
# POST /api/v1/orders?include=shipping_information
+
+{
+  type: 'orders',
+  attributes: { ... }
+}
+ +

This will sideload the shipping information in the response. When using +Spraypaint, do this with:

+ +
order.save({ returnScope: Order.includes('shipping_information') })
+ + + +

+ 8 Context +

+
+ +

All resources have access to #context. If you’re using Rails, +context is the controller instance processing the request.

+ +
# app/resources/post_resource.rb
+attribute :active, :boolean, writable: :admin?
+
+def admin?
+  context.current_user.admin?
+end
+ +

Because current_user is so common, we recommend putting this in +ApplicationResource:

+ +
# app/resources/application_resource.rb
+class ApplicationResource < Graphiti::Resource
+  # ... code ...
+  def current_user
+    context.current_user
+  end
+end
+
+# app/resources/post_resource.rb
+class PostResource < ApplicationResource
+  # ... code ...
+  def admin?
+    current_user.admin?
+  end
+end
+ +

You can manually set context with with_context:

+ +
ctx = OpenStruct.new(current_user: User.first)
+Graphiti.with_context(ctx) do
+  # current_user == ctx.current_user
+  PostResource.all
+end
+ + + +

+ 9 Concurrency +

+
+ +

By default when using Rails, Graphiti will turn on concurrency when ::Rails.application.config.cache_classes is true (the default for staging and production environments). This will cause sibling sideloads to load concurrently. If a Post is sideloading Comments and Author, we’ll load both of those at the same time.

+ +

You can turn on/off this behavior explicitly:

+ +
 # config/intializers/graphiti.rb
+Graphiti.configure do |c|
+  c.concurrency = false
+end
+ +

NOTE: Since this kicks off a new Thread, thread locals will be dropped. So if your code refers to Thread.current[:foo] you should set and get that on Graphiti.context:

+ +
 # BAD:
+Thread.current[:foo] = "bar"
+Thread.current[:foo] # => will be nil when sideloading!
+
+ # GOOD:
+Graphiti.context[:foo] = "bar"
+Graphiti.context[:foo] # => "bar", even when sideloading
+ + + +

+ 10 Adapters +

+
+ +

Common resource overrides can be packaged into an Adapter for code +re-use. The most common example is using a different client/datastore +than ActiveRecord/RelationalDB.

+ +

Adapters are best explained in our ‘Without ActiveRecord’ +Cookbook.

+ +


+

+ +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/concepts/testing.html b/website/static/1.13/guides/concepts/testing.html new file mode 100644 index 00000000..2b4c25e8 --- /dev/null +++ b/website/static/1.13/guides/concepts/testing.html @@ -0,0 +1,1469 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ + + +

+ 1 Overview +

+
+ +

Test first.

+ +

Wait, hear me out!

+ +

Even if you’re not a fan of TDD, Graphiti integration tests are simply the easiest, most pleasant way to develop. +In fact, most Graphiti development can happen without even opening a browser. +And as a side effect, you get a reliable test suite.

+ +

Let’s say we want to filter Employees by title, which comes from +the positions table. Start with a spec:

+ +
RSpec.describe EmployeeResource, type: :resource do
+  describe 'filtering' do
+    context 'by title' do
+      # GIVEN some seed data
+      let!(:employee1) { create(:employee) }
+      let!(:employee2) { create(:employee) }
+      let!(:position1) do
+        create :position,
+          title: 'foo',
+          employee: employee1
+      end
+      let!(:position2) do
+        create :position,
+          title: 'bar',
+          employee: employee2
+      end
+
+      # WHEN a parameter is set
+      before do
+        params[:filter] = { title: 'bar' }
+      end
+
+      # THEN the query results will be correct
+      it 'works' do
+        expect(records.map(&:id)).to eq([employee2.id])
+      end
+    end
+  end
+end
+ +

By developing test-first:

+ +
    +
  • We don’t need to struggle with seeding local development data or +finding the right records for specific scenarios - we can +seed randomized data on-the-fly with factories.
  • +
  • There’s no need to spin up a server and refresh browser pages, +mentally parsing the response payload.
  • +
  • We get a high-confidence test “for free”.
  • +
  • Because our integration test is separate from implementation, we don’t +need to worry about test-induced design damage.
  • +
+ + + +

+ 1.1 API vs Resource +

+
+ +

There are two types of Graphiti tests: API tests and Resource +tests.

+ +

This is because the same Resource logic can be re-used at multiple +endpoints. PostResource can be referenced at /posts, /top_posts, +and /admin/posts, but we shouldn’t have to test the same filtering and +sorting logic over and over. Querying, persistence, and serialization are +all Resource responsibilities, tested in Resource tests.

+ +

We still want API tests, though, to test everything outside of the +Resource: routing, middleware, cache rules, response codes, etc…

+ +

Typically, you’ll write the API test once and not have to touch it +again.

+ + + +

+ 1.2 Factories +

+
+ +
+

Note: Factories are not required, but they are considered a best +practice used by the Graphiti test generator. Read thoughtbot’s +Why Factories? for more +information.

+
+ +

We need to seed data into our test database. To do this, we use Factory +Bot and Faker.

+ +

When you generate a model, a stub factory will be created. It is highly +recommended you edit that factory with randomized data:

+ +
# BEFORE
+FactoryBot.define do
+  factory :employee do
+    first_name { 'MyString' }
+  end
+end
+
+# AFTER
+FactoryBot.define do
+  factory :employee do
+    first_name { Faker::Name.first_name }
+  end
+end
+ +

This will help catch edge cases and provide more clarity than seeing the +same "MyString" everywhere.

+ +

It’s a best practice that if a factory defines an attribute, there +should be a corresponding validation around that attribute. If an +attribute is optional, it should not be defaulted in a factory.

+ +

Finally, Rails 5 made belongs_to required by default. This means that if Employee belongs_to :department, then create(:employee) will fail. To ensure a relationship is always seeded:

+ +
FactoryBot.define do
+  factory :employee do
+    department
+    # OR association :department, factory: :department
+  end
+end
+ + + +

+ 1.3 RSpec +

+
+ +

RSpec is not required, but considered a first-class citizen used by the +Graphiti test generator.

+ + + +

+ 2 Test Helpers +

+
+ +

Tests are run using JSONAPI standards. But the +JSONAPI payload can be a pain to deal with. So, we’ve supplied helpers.

+ +

These helpers ship with Graphiti, under Graphiti::SpecHelpers.

+ + + +

+ 2.1 #jsonapi_data +

+
+ +
+

Note: for brevity, this method is aliased to d

+
+ +

The jsonapi_data method will parse response data and return a +normalized object (Graphiti::SpecHelpers::Node). Assert against this the same way you assert against +JSON:

+ +
data = jsonapi_data[0]
+expect(data.id).to eq(employee.id)
+expect(data.jsonapi_type).to eq('employees')
+expect(data.first_name).to eq('Jane')
+ +
    +
  • id will automatically case to an integer. If you would like to avoid +this, use rawid instead.
  • +
  • jsonapi_type is a convenience method for data/type, to avoid +conflicting with an attribute of the same name.
  • +
  • If the first_name key was not present in the response, an error will +be raised.
  • +
+ + + +

+ 2.2 Accessing Sideloads +

+
+ +

To grab a relationship:

+ +
sideload = d[0].sideload(:comments)
+expect(sideload.id).to eq(123)
+expect(sideload.jsonapi_type).to eq('comments')
+expect(sideload.body).to eq('body')
+ +

The sideload method accepts the name of the relationship. It returns +a normal jsonapi_data Graphiti::SpecHelpers::Node containing the include-ed data.

+ + + +

+ 2.3 Accessing Links +

+
+ +

To grab a Link:

+ +
d[0].link(:comments, :related)
+ +

This accepts the relationship name and the link type. It will return the +link URL.

+ + + +

+ 2.2 #json +

+
+ +

To see the raw JSON response, use json.

+ + + +

+ 2.3 #date and #datetime +

+
+ +

In Graphiti, datetimes are rendered in ISO 8601 format. This means that straight date comparisons will fail:

+ +
# WRONG
+expect(d[0].created_at).to eq(post.created_at)
+ +

Instead, use the datetime helper to convert to ISO 8601 and compare +apples to apples:

+ +
# RIGHT
+expect(d[0].created_at).to eq(datetime(post.created_at))
+ +

Similarly, there’s a date helper as well.

+ + + +

+ 2.4 #jsonapi_errors +

+
+ +
+

This method is aliased to errors for brevity

+
+ +

To parse an Errors Payload:

+ +
errors = jsonapi_errors
+
+# Direct access
+expect(errors.length).to eq(1)
+expect(errors[0].attribute).to eq(:name)
+expect(errors[0].status).to eq('422')
+expect(errors[0].title).to eq('Validation Error')
+expect(errors[0].detail).to eq("Name can't be blank")
+expect(errors[0].code).to eq(:blank)
+expect(errors[0].message).to eq("can't be blank")
+
+# By attribute
+expect(errors.name.message).to eq("can't be blank")
+expect(errors.name.code).to eq(:blank)
+# ... etc ...
+
+# As a hash
+expect(errors.to_h).to eq({
+  name: "can't be blank"
+})
+ + + +

+ 2.5 Resource Test Helpers +

+
+ +

Resource tests have two helpers, both different ways to execute a query.

+ +

render will fire the query and return a JSON response that can be +accessed as normal:

+ +
it 'works' do
+  render
+  expect(d[0].first_name).to eq('Jane')
+  json # => { data: { type: 'employees', ... } }
+end
+ +

records will return model instances:

+ +
it 'works' do
+  render
+  expect(records.map(&:id)).to eq([1, 2, 3])
+end
+ + + +

+ 2.6 API Test Helpers +

+
+ +

When executing an API test request, always use the jsonapi_ doppelgänger:

+ +
    +
  • jsonapi_get(url, params:) instead of get
  • +
  • jsonapi_post(url, payload) instead of post
  • +
  • jsonapi_put(url, payload) instead of put
  • +
  • jsonapi_patch(url, payload) instead of patch
  • +
  • jsonapi_delete(url) instead of delete
  • +
+ +

This will set the CONTENT_TYPE header to application/vnd.api+json +and call to_json on the payload (when applicable).

+ +

It also allows overriding jsonapi_headers. Use this to manipulate +headers for a given request:

+ +
def jsonapi_headers
+  super.tap do |headers|
+    headers['CUSTOM'] = 'foo'
+  end
+end
+ + + +

+ 2.7 Guard Helpers +

+
+ +

Many teams use guard in development to watch +their project files and run a smaller set of focused tests as code changes. For +those teams leveraging guard and the guard-rspec plugin, +we offer an additional set of DSL helpers via the +guard-rspec-graphiti plugin. +For more details, check out the project README.

+ + + +

+ 3 Resource Tests +

+
+ +

There are two test files for each Resource:

+ +
    +
  • spec/resources/post/reads_spec.rb
  • +
  • spec/resources/post/writes_spec.rb
  • +
+ + + +

+ 3.1 Reads +

+
+ +

The basic setup for read operations:

+ +
# spec/resources/employee/reads_spec.rb
+require 'rails_helper'
+
+RSpec.describe EmployeeResource, type: :resource do
+  describe 'serialization' do
+    # ... code ...
+  end
+
+  describe 'filtering' do
+    # ... code ...
+  end
+
+  describe 'sorting' do
+    # ... code ...
+  end
+
+  describe 'sideloading' do
+    # ... code ...
+  end
+end
+ + + +

+ 3.1.1 Serialization +

+
+ +
describe 'serialization' do
+  let!(:employee) { create(:employee, first_name: 'Jane') }
+
+  it 'works' do
+    render
+    data = jsonapi_data[0]
+    expect(data.id).to eq(employee.id)
+    expect(data.jsonapi_type).to eq('employees')
+    expect(data.first_name).to eq('Jane')
+  end
+end
+ +

We want to test that our attributes render correctly. We’ll do this by +seeding a record, firing a basic query, and comparing the JSON result to +the seeded data.

+ +

Best practices:

+ +
    +
  • Assert on all attributes, even if there is no logic. This way adding +logic will cause a test failure.
  • +
  • When seeding data, manually assign values. This way you can be assured +you aren’t accidentally testing nil == nil
  • +
+ +

If you decide you have a high level of confidence in your factories, you +can instead save some keystrokes and assert on randomized data:

+ +
expect(data.first_name).to eq(employee.first_name)
+ +
+

Note: Our schema validation test will ensure no attributes get +removed or change types.

+
+ + + +

+ 3.1.2 Filtering +

+
+ +
describe 'filtering' do
+  let!(:employee1) { create(:employee) }
+  let!(:employee2) { create(:employee) }
+
+  context 'by id' do
+    before do
+      params[:filter] = { id: { eq: employee2.id } }
+    end
+
+    it 'works' do
+      render
+      expect(d.map(&:id)).to eq([employee2.id])
+    end
+  end
+end
+ +

Here we seed data, set the filter parameter, and assert only records +matching the given criteria are present in the response.

+ +

In general, you only need to test filtering when there is custom logic. +Our schema validation test will ensure no filters are removed, guarded, +changed operators, etc.

+ + + +

+ 3.1.3 Sorting +

+
+ +
describe 'sorting' do
+  describe 'by id' do
+    let!(:employee1) { create(:employee) }
+    let!(:employee2) { create(:employee) }
+
+    context 'when ascending' do
+      before do
+        params[:sort] = 'id'
+      end
+
+      it 'works' do
+        render
+        expect(d.map(&:id)).to eq([
+          employee1.id,
+          employee2.id
+        ])
+      end
+    end
+
+    context 'when descending' do
+      before do
+        params[:sort] = '-id'
+      end
+
+      it 'works' do
+        render
+        expect(d.map(&:id)).to eq([
+          employee2.id,
+          employee1.id
+        ])
+      end
+    end
+  end
+end
+ +

Here we seed data, set the sort parameter, and assert the correct order +of the rendered response.

+ +

In general, you only need to test sorting when there is custom logic. +Our schema validation test will ensure no sorts are removed, guarded or +limited in direction.

+ + + +

+ 3.1.4 Sideloading +

+
+ +
describe 'sideloading' do
+  let!(:employee) { create(:employee) }
+
+  describe 'current_position' do
+    let!(:pos1) do
+      create(:position, employee: employee, historical_index: 2)
+    end
+    let!(:pos2) do
+      create(:position, employee: employee, historical_index: 1)
+    end
+
+    before do
+      params[:include] = 'current_position'
+    end
+
+    it 'returns position with historical index == 1' do
+      render
+      sl = d[0].sideload(:current_position)
+      expect(sl.jsonapi_type).to eq('positions')
+      expect(sl.id).to eq(pos2.id)
+    end
+  end
+end
+ +

Here we seed data, set the sideload parameter, and assert the correct +entity is present in the request. There is no need to test each +attribute of the sideload - this should be tested in the Resource +Test of the sideloaded Resource.

+ +

In general, you only need to test sideloads when there is custom logic. +Our schema validation test will ensure no sideloads are removed or +associated to a different Resource.

+ + + +

+ 3.2 Writes +

+
+ +

The basic setup for write operations:

+ +
# spec/resources/employee/writes_spec.rb
+require 'rails_helper'
+
+RSpec.describe EmployeeResource, type: :resource do
+  describe 'creating' do
+    let(:payload) { ... }
+    # ... code ...
+  end
+
+  describe 'creating' do
+    let(:payload) { ... }
+    # ... code ...
+  end
+
+  describe 'destroying' do
+    # ... code ...
+  end
+end
+ +

Here payload is a JSONAPI Resource Object.

+ + + +

+ 3.2.1 Create +

+
+ +
describe 'creating' do
+  let(:payload) do
+    {
+      data: {
+        type: 'employees',
+        attributes: { }
+      }
+    }
+  end)
+
+  let(:instance) do
+    EmployeeResource.build(payload)
+  end
+
+  it 'works' do
+    expect {
+      expect(instance.save).to eq(true)
+    }.to change { Employee.count }.by(1)
+  end
+end
+ +

Here payload is an empty Employee Resource Object. +We’ll assert that when saving this empty payload, an Employee is +created.

+ +

You’ll likely want to add attributes here and ensure they are persisted +correctly:

+ +
let(:payload) do
+  {
+    data: {
+      type: 'employees',
+      attributes: { first_name: 'Jane', age: 30 }
+    }
+  }
+end
+
+# ... code ...
+
+it 'works' do
+  expect {
+    expect(instance.save).to eq(true)
+  }.to change { Employee.count }.by(1)
+  employee = Employee.last
+  expect(employee.first_name).to eq('Jane')
+  expect(employee.age).to eq(30)
+end
+ + + +
+ 3.2.1.1 Required Belongs To +
+
+ +

Rails 5 made belongs_to required by default. This means that if Employee belongs_to :department, the above tests will fail (we cannot create the Employee without associating it to Department).

+ +

You have 3 options here:

+ +
    +
  • Turn off this validation in test mode. Add config.active_record.belongs_to_required_by_default = false to config/environments/test.rb.
  • +
  • Turn off the validation for this specific relationship: belongs_to :department, optional: true.
  • +
  • Associate as part of the request.
  • +
+ +

We recommend the third option to preserve real-world end-to-end +behavior:

+ +
describe 'creating' do
+  let!(:department) { create(:department) }
+
+  let(:payload) do
+    {
+      type: 'employees',
+      attributes: { ... },
+      relationships: {
+        department: {
+          data: {
+            type: 'departments',
+            id: department.id.to_s
+          }
+        }
+      }
+    }
+  end
+
+  # ... code ...
+end
+ +

Will ensure the Employee is created and associated to the given +department.

+ + + +

+ 3.2.2 Update +

+
+ +
describe 'updating' do
+  let!(:employee) { create(:employee) }
+
+  let(:payload) do
+    {
+      data: {
+        id: employee.id.to_s,
+        type: 'employees',
+        attributes: { first_name: 'changed!' }
+      }
+    }
+  end
+
+  let(:instance) do
+    EmployeeResource.find(payload)
+  end
+
+  it 'works' do
+    expect {
+      expect(instance.update_attributes).to eq(true)
+    }.to change { employee.reload.updated_at }
+     .and change { employee.first_name }.to('changed!')
+  end
+end
+ +
+

Note that this test will be pending by default when using the +generator, as we require the attributes to be explicitly defined.

+
+ +

Here payload is an empty Employee Resource Object. +We’ll assert that when updating attributes, the changes are correctly +persisted to the database.

+ + + +

+ 3.2.3 Destroy +

+
+ +
describe 'destroying' do
+  let!(:employee) { create(:employee) }
+
+  let(:instance) do
+    EmployeeResource.find(id: employee.id)
+  end
+
+  it 'works' do
+    expect {
+      expect(instance.destroy).to eq(true)
+    }.to change { Employee.count }.by(-1)
+  end
+end
+ +

Here we ensure that a delete request correctly removes a record from the +database.

+ + + +

+ 3.2.4 Side Effects +

+
+ +
it 'works' do
+  # some assertion
+  email = ActionMailer::Base.deliveries.last
+  expect(email.subject).to eq('Welcome!')
+end
+ +

It’s common for write operations to cause side-effects, such as sending +an email or updating an audit trail. It’s recommended to test these +within the same “it” block unless the logic gets particularly intense. +Though “one expectation per test” works well for unit tests, integration +tests can take longer to run and the performance penalty isn’t worth it.

+ + + +

+ 4 API Tests +

+
+ +

There are five test files for each Resource:

+ +
    +
  • spec/api/v1/employees/index_spec.rb
  • +
  • spec/api/v1/employees/show_spec.rb
  • +
  • spec/api/v1/employees/create_spec.rb
  • +
  • spec/api/v1/employees/update_spec.rb
  • +
  • spec/api/v1/employees/destroy_spec.rb
  • +
+ + + +

+ 4.1 Reads +

+
+ + + +

+ 4.1.1 #index +

+
+ +
require 'rails_helper'
+
+RSpec.describe "employees#index", type: :request do
+  let(:params) { {} }
+
+  subject(:make_request) do
+    jsonapi_get "/api/v1/employees", params: params
+  end
+
+  describe 'basic fetch' do
+    let!(:employee1) { create(:employee) }
+    let!(:employee2) { create(:employee) }
+
+    it 'works' do
+      expect(EmployeeResource).to receive(:all).and_call_original
+      make_request
+      expect(response.status).to eq(200)
+      expect(d.map(&:jsonapi_type).uniq)
+        .to match_array(['employees'])
+      expect(d.map(&:id))
+        .to match_array([employee1.id, employee2.id])
+    end
+  end
+end
+ +

Here we’re ensuring EmployeeResource is the correct resource to be +called from this endpoint, we get a 200 status code, and the entities +returned are expected.

+ + + +

+ 4.1.2 #show +

+
+ +
require 'rails_helper'
+
+RSpec.describe "employees#show", type: :request do
+  let(:params) { {} }
+
+  subject(:make_request) do
+    jsonapi_get "/api/v1/employees/#{employee.id}", params: params
+  end
+
+  describe 'basic fetch' do
+    let!(:employee) { create(:employee) }
+
+    it 'works' do
+      expect(EmployeeResource).to receive(:find).and_call_original
+      make_request
+      expect(response.status).to eq(200)
+      expect(d.jsonapi_type).to eq('employees')
+      expect(d.id).to eq(employee.id)
+    end
+  end
+end
+ +

Similar to index, but fetching only a single Employee.

+ + + +

+ 4.2 Writes +

+
+ + + +

+ 4.2.1 #create +

+
+ +
require 'rails_helper'
+
+RSpec.describe "employees#create", type: :request do
+  subject(:make_request) do
+    jsonapi_post "/api/v1/employees", payload
+  end
+
+  describe 'basic create' do
+    let(:payload) do
+      {
+        data: {
+          type: 'employees',
+          attributes: {
+            first_name: 'Jane'
+          }
+        }
+      }
+    end
+
+    it 'works' do
+      expect(EmployeeResource).to receive(:build).and_call_original
+      expect {
+        make_request
+      }.to change { Employee.count }.by(1)
+      expect(response.status).to eq(201)
+    end
+  end
+end
+ +

Here we’re ensuring EmployeeResource is called, a record is correctly +inserted, and the response code is 201.

+ +

You probably only want to add attributes required to pass validation, +here - note that we don’t assert on attributes of the created record +(save this for your Resource test). One easy way to do this is to pass +randomized data from your factory:

+ +
let(:payload) do
+  {
+    data: {
+      type: 'employees',
+      attributes: attributes_for(:employee)
+    }
+  }
+end
+ +

See also:

+ + + + + +

+ 4.2.2 #update +

+
+ +
require 'rails_helper'
+
+RSpec.describe "employees#update", type: :request do
+  subject(:make_request) do
+    jsonapi_put "/api/v1/employees/#{employee.id}", payload
+  end
+
+  describe 'basic update' do
+    let!(:employee) { create(:employee) }
+
+    let(:payload) do
+      {
+        data: {
+          id: employee.id.to_s,
+          type: 'employees',
+          attributes: {
+            first_name: 'changed!'
+          }
+        }
+      }
+    end
+
+    it 'updates the resource' do
+      expect(EmployeeResource).to receive(:find).and_call_original
+      expect {
+        make_request
+      }.to change { employee.reload.attributes }
+      expect(response.status).to eq(200)
+    end
+  end
+end
+ +

Here we’re ensuring EmployeeResource is called, attributes are updated, +and we respond with a 201. Note that we don’t assert on specific +attributes - save that for your Resource test.

+ +

Just like the prior section, you may want to leverage FactoryBot here to +generate randomized attributes:

+ +
let(:payload) do
+  {
+    data: {
+      id: employee.id.to_s,
+      type: 'employees',
+      attributes: attributes_for(:employee)
+    }
+  }
+end
+ + + +

+ 4.2.3 #destroy +

+
+ +
require 'rails_helper'
+
+RSpec.describe "employees#destroy", type: :request do
+  subject(:make_request) do
+    jsonapi_delete "/api/v1/employees/#{employee.id}"
+  end
+
+  describe 'basic destroy' do
+    let!(:employee) { create(:employee) }
+
+    it 'updates the resource' do
+      expect(EmployeeResource).to receive(:find).and_call_original
+      expect { make_request }.to change { Employee.count }.by(-1)
+      expect { employee.reload }
+        .to raise_error(ActiveRecord::RecordNotFound)
+      expect(response.status).to eq(200)
+      expect(json).to eq('meta' => {})
+    end
+  end
+end
+ +

Here we’re sending a DELETE request, ensuring the record is actually +removed, and we respond according to the JSONAPI specification.

+ + + +

+ 5 Context +

+
+ +

Occasionally you’ll need to set context for tests. The most common +scenario is authorization:

+ +
attribute :salary, :integer, readable: :admin?
+
+def admin?
+  context.current_user.admin?
+end
+ +

When using Rails, context is the controller associated to the request. +We can manually set context in tests:

+ +
let(:user) { double(admin?: true) }
+let(:ctx) { double(current_user: user) }
+
+it 'works' do
+  Graphiti.with_context ctx do
+    render
+  end
+  expect(d[0].salary).to eq(100_000)
+end
+ +


+ + + +

+ 6 Schema Validation +

+
+ +

Graphiti comes with built-in backwards-compatibility tests. We do this +by comparing the current version of the schema with one previously +checked-in.

+ +

These tests are added at the bottom of spec/rails_helper.rb:

+ +
Graphiti::SpecHelpers::RSpec.schema!
+ +

Whenever you run tests, the schema check will also run. If we find any +backwards-incompatibilities - attributes removed, types changed, default +sort direction modified, etc - the schema test will fail with an output +detailing all incompatibilities.

+ +

When the schema test succeeds, it will overwrite the existing schema +file with the new schema. It will not do this on failure.

+ +

There are times when you want to accept an incompatibility and move on +anyway. In this case, use FORCE_SCHEMA:

+ +
$ FORCE_SCHEMA=true bin/rspec
+ +


+ + + +

+ 7 Testing Spectrum +

+
+ +

Testing standards vary from team to team, and there is no right answer +when judging “the right level of testing”.

+ +

You could add tests for every attribute, validating every sort and +filter. Or, you could consider logicless configuration tested as part of +Graphiti itself (the same way we don’t tend to test a has_many +ActiveRecord relationship). Though our guides favor the latter, the +extra tests could prove useful when performing a major upgrade or +swapping datastores.

+ +

You could do more API testing, particularly for high-value +functionality. Testing fully end-to-end, from middleware to response +codes, gives a high level of confidence. But it can also feel like +duplicate tests across endpoints, which is why we have Resource tests.

+ +

Graphiti provides sensible defaults, but you’re encouraged to consider +the tradeoffs and pick the right level of testing for you.

+ + + +

+ 8 Double-Testing Units +

+
+ +

Integration testing is great: it gives a high level of confidence, and +they’re typically the easiest tests to write. In fact, these tests are +so powerful the value of unit testing sometimes comes up for debate.

+ +

Consider a custom filter powered by an ActiveRecord scope:

+ +
# app/resources/employee_resource.rb
+filter :title, :string do
+  eq do |scope, value|
+    scope.by_title(value)
+  end
+end
+
+# app/models/employee.rb
+scope :by_title, ->(title) {
+  joins(:current_position)
+    .where("lower(title) = ?", title.downcase)
+}
+ +

If we’re by-the-book, we should absolutely test .by_title on the +Employee model. After all, we’re exposing a public interface that other +developers might rely on in the future.

+ +

This can feel cumbersome, even duplicative. The Resource Test of the +title filter will seed the same data as the corresponding unit test, and +the assertion will be almost identical. But because Resource Tests are +integration tests, we shouldn’t mock the code either.

+ +

The best practice here is to use RSpec shared_context to remove the duplication:

+ +
# spec/support/employees_helper.rb
+RSpec.shared_context 'employees by title' do
+  let!(:employee1) { create(:employee) }
+  let!(:employee2) { create(:employee) }
+  let!(:employee3) { create(:employee) }
+  let!(:position1) do
+    create(:position, title: 'foo', employee: employee1)
+  end
+  let!(:position2) do
+    create(:position, title: 'BAR', employee: employee2)
+  end
+  let!(:position3) do
+    create(:position, title: 'bar', employee: employee3)
+  end
+end
+
+# spec/models/employee.rb
+describe '.by_title' do
+  include_context 'employees by title'
+
+  it 'returns employees matching the given title' do
+    expect(Employee.by_title('bar'))
+      .to eq([employee2, employee3])
+  end
+end
+
+# spec/resources/employee_resource.rb
+describe 'filtering' do
+  describe 'by title' do
+    include_context 'employees by title'
+
+    before do
+      params[:filter] = { title: 'bar' }
+    end
+
+    it 'returns employees matching the given title' do
+      expect(records).to eq([employee2, employee3])
+    end
+  end
+end
+ +

This allows our by_title scope to be re-used by future developers +outside of the Resource context. It also keeps code clean and +isolated.

+ +

But it’s not unreasonable to think the overhead here isn’t worth it. If +you’re of this mind, we recommend testing the Resource and marking the +method as not re-usable:

+ +
# @api private
+scope :by_title, ->(value) { ... }
+ +

This way future developers know the scope is only an implementation +detail and not considered part of this object’s public API. Writing the +unit test can be deferred until the use case actually arises.

+ + + +

+ 9 Generators +

+
+ +

The Resource generator will create both Resource and API tests for you. +Use these as templates to implement your tests.

+ +

You can also run

+ +
$ rails generate graphiti:api_test RESOURCE [options]
+ +

For example

+ +
$ rails generate graphiti:api_test EmployeeResource -a index show
+ +

To generate only the API tests. This can be particularly helpful because +API tests are mostly boilerplate that does not need to be manually +edited. Pass the -a option to limit RESTful actions.

+ +


+

+
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/getting-started/installation.html b/website/static/1.13/guides/getting-started/installation.html new file mode 100644 index 00000000..da005281 --- /dev/null +++ b/website/static/1.13/guides/getting-started/installation.html @@ -0,0 +1,420 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ + + +

+ 1 From Scratch +

+
+ +

The easiest way to start from scratch is to use the application +template:

+ +
$ rails new blog --api -m https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb
+ +

Alternatively, download and point to the template locally:

+ +
$ curl -O https://raw.githubusercontent.com/graphiti-api/graphiti_rails_template/master/all.rb
+$ rails new blog --api -m all.rb
+ +

Run git diff to see the changes to a blank Rails app.

+ + + +

+ 2 Adding to an Existing App +

+
+ +

This process is straightforward; you can add Graphiti to an existing +Rails app alongside JBuilder or ActiveModelSerializers.

+ +

Start with gems:

+ +
# The only strictly-required gem
+gem 'graphiti'
+
+# For automatic ActiveRecord pagination
+gem 'kaminari'
+
+# Test-specific gems
+group :development, :test do
+  gem 'rspec-rails'
+  gem 'factory_bot_rails'
+  gem 'faker'
+end
+
+group :test do
+  gem 'database_cleaner'
+end
+ +

You’ll be up-and-running at this point. Verify with a simple standalone +Resource:

+ +
# Assuming you already have a Post ActiveRecord Model
+class PostResource < Graphiti::Resource
+  self.adapter = Graphiti::Adapters::ActiveRecord
+  attribute :title, :string
+end
+
+PostResource.all.data # => [#<Post>, #<Post>, ...]
+ +

Now we just need to integrate with Rails endpoints (to give us things +like #context):

+ +
# app/controllers/application_controller.rb
+class ApplicationController < ActionController::Base
+  include Graphiti::Rails::Controller
+end
+ +

And wire-up our error-handling:

+ +
# app/controllers/application_controller.rb
+# When #show action does not find record, return 404
+register_exception Graphiti::Errors::RecordNotFound,
+  status: 404
+
+rescue_from Exception do |e|
+  handle_exception(e)
+end
+ +

That’s it for the basics. You may have issues with generators +conflicting with your existing application structure - but you can +always write files manually or submit an issue.

+ + + +

+ 2.1 Responders +

+
+ +

Graphiti supports JSONAPI, simple JSON, and XML. You can do this +manually when inheriting from ActionController::Base

+ +
def index
+  posts = PostResource.all(params)
+
+  respond_to do |format|
+    format.json { render(json: posts) }
+    format.jsonapi { render(jsonapi: posts) }
+    format.xml { render(xml: posts) }
+  end
+end
+ +

But we can inherit from ActionController::API while avoiding this +boilerplate with with the Responders gem:

+ +
def index
+  posts = PostResource.all(params)
+  respond_with(posts)
+end
+ +

To get this functionality:

+ +
# Gemfile
+gem 'responders'
+
+# app/controllers/application_controller.rb
+include Graphiti::Rails::Responders
+ +
+

Note: Persistence operations only support JSONAPI format, so you’ll +still use render jsonapi: and render jsonapi_errors: for those.

+
+ + + +

+ 2.3 .graphiticfg.yml +

+
+ +

The .graphiticfg.yml file lives in the root directory of your +application. It holds configuration we need to reuse across a variety of +contexts (primarily generates and rake tasks). If you use our template to create your application, it’s created for you.

+ +

Primarily this is used to hold your “API namespace”:

+ +
---
+namespace: /my_api/v1
+ +

If this file doesn’t exist you may get unexpected errors - make sure to +create it!

+ + + +

+ 2.4 Testing +

+
+ +

To add our Integration Tests:

+ +
# Gemfile
+group :development, :test do
+  gem 'factory_bot_rails'
+  gem 'rspec_rails'
+  gem 'faker'
+end
+
+group :test do
+  gem 'database_cleaner'
+end
+ +

Bootstrap RSpec if you haven’t already:

+ +
$ bin/rails g rspec:install
+ +

Add some RSpec configuration:

+ +
require 'graphiti/spec_helpers/rspec'
+
+RSpec.configure do |config|
+  config.include FactoryBot::Syntax::Methods
+  config.include Graphiti::SpecHelpers::RSpec
+  config.include Graphiti::SpecHelpers::Sugar
+  config.include Graphiti::Rails::TestHelpers, type: :request
+
+  # Clean your DB between test runs
+  config.before(:suite) do
+    DatabaseCleaner.strategy = :transaction
+    DatabaseCleaner.clean_with(:truncation)
+  end
+
+  config.around(:each) do |example|
+    begin
+      DatabaseCleaner.cleaning do
+        example.run
+      end
+    ensure
+      DatabaseCleaner.clean
+    end
+  end
+end
+ + + +

+ 2.5 will_paginate +

+
+ +

By default, we use Kaminari for +ActiveRecord pagination. If you prefer [will_paginate] (or anything +else):

+ +
# app/resources/application_resource.rb
+paginate do |scope, current_page, per_page|
+  scope.paginate(page: current_page, per_page: per_page)
+end
+ + + +

+ 3 Without Rails +

+
+ +

You can use Graphiti in any plain .rb file. To see this in action, +check out the Plain Ruby Sample App.

+ +


+

+
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/graphiti-rails-migration.html b/website/static/1.13/guides/graphiti-rails-migration.html new file mode 100644 index 00000000..30f70210 --- /dev/null +++ b/website/static/1.13/guides/graphiti-rails-migration.html @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Migrating to graphiti-rails

+ +

You can continue using Graphiti 1.0 without this, but the migration +is only a few simple steps.

+ +

Application Code

+ +

First, bump to Graphiti 1.2 - everything should work as-is, but you’ll +start seeing deprecation notices.

+ +

Then add graphiti-rails to your Gemfile:

+ +
gem 'graphiti-rails', '~> 0.1'
+ +

You’ll see slightly different deprecation notices at this point. To fix:

+ +
    +
  • Remove include Graphiti::Rails from ApplicationController
  • +
  • Remove include Graphiti::Responders from ApplicationController
  • +
  • Remove the register_exception for 404 errors if you have one. Keep +anything else, but this is now built-in by default.
  • +
  • If you’re using responders, Add include Graphiti::Rails::Responders from ApplicationController.
  • +
  • Remove the rescue_from block.
  • +
+ +

Finally, if you were passing the show_raw_error option to +#handle_exception, replace this with #show_detailed_exceptions?:

+ +
def show_detailed_exceptions?
+  current_user.admin?
+end
+ +
+

Keep in mind, we’ll always show detailed exceptions in development +mode, per Rails conventions. If this is confusing or not desirable, +set config.consider_all_requests_local = false in +config/environments/development.rb

+
+ +
+

If you’re not running Rails in API-only mode, be sure to set +config.debug_exception_response_format = :api in +config/application.rb

+
+ +

Testing

+ +

Add config.include Graphiti::Rails::TestHelpers to +spec/rails_helper.rb.

+ +

Replace

+ +
config.before :each do
+  GraphitiErrors.disable!
+end
+ +

With

+ +
config.before :each do
+  handle_request_exceptions(false)
+end
+ +

Change any calls to GraphitiErrors.enable!/disable! with +handle_request_exceptions(true|false).

+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/index.html b/website/static/1.13/guides/index.html new file mode 100644 index 00000000..7b511afb --- /dev/null +++ b/website/static/1.13/guides/index.html @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Guides

+ +
+ +
+ +

Graphiti makes RESTful Resources a first-class concept. This enables +reading and writing a graph of data in a single request, a schema with +backwards-compatible guarantee, end-to-end integration test +patterns, seamless microservices and much more.

+ +

If you just want to get a 5-minute feel for Graphiti code and all the +functionality that comes out-of-the box, head to the +Quickstart. If you want a birds-eye view of Graphiti’s +moving pieces, check out our Intro to Graphiti video or The Lifecycle of a Graphiti +Request. Or to understand Resources, +Graphiti’s core concept, see the Resource +API.

+ +

Or to dive into code, check out our sample apps in Rails, Sinatra and Plain Ruby.

+ +

But ideally, read everything 🙂

+ + + +


+

+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/overview.html b/website/static/1.13/guides/overview.html new file mode 100644 index 00000000..c80b025a --- /dev/null +++ b/website/static/1.13/guides/overview.html @@ -0,0 +1,325 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

2 Lifecycle of a Graphiti Request

+ +

+ +

+ +


+ +

The key concept here is a Resource. Resources sit between a Request and a Backend, defining how to query, persist, and serialize data. They are accessible through Endpoints, which customize Resource behavior based on context.

+ +


+ +

+ +

+ +

Each Resource is comprised of Attributes. Each Attribute corresponds to behavior for:

+ +
    +
  • Reading: The fields rendered in the response, e.g. { "first_name": "Jane" }
  • +
  • Writing: The fields accepted in the payload, e.g. a POST or PUT request.
  • +
  • Filtering: The fields we can query, e.g. /employees?filter[first_name]=Jane
  • +
  • Sorting: The fields we can sort on, e.g. /employees?sort=age
  • +
+ +

Each Attribute has a Type, which will be coerced and checked at runtime.

+ +

+ +

+ +

A Resource does not mean “a database table”, though the two have a lot in common and often match. A Resource is a generic interface wrapping a Backend. That Backend could be a relational database, a No-SQL database, or even a third-party service call. And you can use whichever client or ORM you’d like for a given Backend (ActiveRecord, Sequel, Mongoid, Net::HTTP, etc).

+ +

Resources define an interface for querying from and persisting to a given Backend. The generic, common, cross-Resource logic for connecting a Resource to a Backend is defined in an Adapter; individual Resources can override Adapter logic. The default adapter is ActiveRecord (Graphiti::Adapters::ActiveRecord).

+ +

From the raw backend results, the Resource builds Models, which hold +business logic. In other words you might query using elasticsearch-ruby, but return POROs as the result of that query. In the case of ActiveRecord, Backend and Model are the same thing.

+ +

Finally, these Models are serialized when we actually render a response. You’ll still use Rails, Sinatra, or whatever-else to manage routing, HTTP codes, etc.

+ +

+ +

+ +

Critically, each Resource can connect to other Resources. This can occur through Sideloading (“fetch the employee, her positions, and the departments for those positions in a single request”), Sideposting (“save the employee and her positions/departments in a single request), and Links (“here’s a URL to lazy-load positions in a separate request”). Because Resources connect to each other, this is why we often refer to a Resource as a “node in the graph”.

+ +

Any logic used for fetching a single Resource can be re-used when fetching multiple Resources. In other words, you can say “fetch me the Employee, and her last three Positions ordered by created_at”. Applying query logic to nested levels of the graph is called Deep Querying.

+ +

Graphiti does not depend on Rails, but it is a first-class citizen. +Given the code:

+ +
# config/routes.rb
+resources :employees
+ +
# app/controllers/employees_controller.rb
+class EmployeesController < ApplicationController
+  def index
+    employees = EmployeeResource.all(params)
+    respond_with(employees)
+  end
+end
+ +
# app/models/employee.rb
+class Employee < ApplicationRecord
+end
+ +
# app/resources/employee_resource.rb
+class EmployeeResource < ApplicationResource
+  attribute :name, :string
+  attribute :age, :integer
+end
+ +

We can now

+ +
    +
  • Fetch JSONAPI, Simple JSON, or XML +
      +
    • /employees.jsonapi
    • +
    • /employees.json
    • +
    • /employees.xml
    • +
    +
  • +
  • Paginate: +
      +
    • /employees?page[size]=10&page[number]=2
    • +
    +
  • +
  • Sort: +
      +
    • Ascending: /employees?sort=age
    • +
    • Descending: /employees?sort=-age
    • +
    +
  • +
  • Filter: +
      +
    • Case Insensitive: /employees?filter[name]=jane doe
    • +
    • Case Sensitive: /employees?filter[name][eql]=Jane Doe
    • +
    • Prefix: /employees?filter[name][prefix]=graph
    • +
    • Suffix: /employees?filter[name][suffix]=rocks
    • +
    • Contains: /employees?filter[name][match]=iti
    • +
    • Equal: /employees?filter[age]=100
    • +
    • Greater Than: /employees?filter[age][gt]=100
    • +
    • Greater Than or Equal To: /employees?filter[age][gte]=100
    • +
    • Less Than: /employees?filter[age][lt]=100
    • +
    • Less Than or Equal To: /employees?filter[age][lte]=100
    • +
    +
  • +
  • Only return specific fields: +
      +
    • /employees?fields[employees]=name,age
    • +
    +
  • +
  • Get total count: +
      +
    • /employees?stats[total]=count
    • +
    +
  • +
+ +

To add a relationship:

+ +
# app/models/employee.rb
+has_many :positions
+ +
# app/models/position.rb
+class Position < ApplicationRecord
+  belongs_to :employee
+end
+ +
# app/resources/employee_resource.rb
+has_many :positions
+ +
# app/resources/position_resource.rb
+class PositionResource < ApplicationResource
+  attribute :title, :string
+  belongs_to :employee
+end
+ +

We can now fetch Posts and Comments in a single request - including sorting the comments, filtering, fieldsets and everything else a Resource supports.

+ +


+ +

+ +

+ +


+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/upgrading-2-0.html b/website/static/1.13/guides/upgrading-2-0.html new file mode 100644 index 00000000..ed014566 --- /dev/null +++ b/website/static/1.13/guides/upgrading-2-0.html @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Upgrading to Graphiti 2.0

+ +

Graphiti 2.0 requires Ruby 3.2+ and Rails 7.1+, and folds graphiti-rails, graphiti_spec_helpers and graphiti_errors into graphiti itself. Those three gems are retired.

+ +

For most apps the upgrade is three things:

+ +
    +
  1. Remove graphiti-rails, graphiti_spec_helpers and graphiti_errors from your Gemfile.
  2. +
  3. Add include Graphiti::Rails::Controller to the controllers serving your resources.
  4. +
  5. Update around_persistence hooks, if you have any — they now receive the model rather than the attributes hash.
  6. +
+ +

Old names still resolve and warn, so nothing breaks halfway through. They are removed in 3.0.

+ +

The full upgrade guide lists every rename, what was removed outright, and the migration path for apps not running Rails.

+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/upgrading.html b/website/static/1.13/guides/upgrading.html new file mode 100644 index 00000000..7ec6e25a --- /dev/null +++ b/website/static/1.13/guides/upgrading.html @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Upgrading from JSONAPI Suite

+ +

The early version of Graphiti was JSONAPI Suite. If you are a JSONAPI +Suite user, here’s how to upgrade your existing API.

+ +

The work here is mostly consolidating logic that currently lives in +multiple files into a single Resource, and rewriting specs. The good +news is that, because we emphasize full-stack integration tests, we can +perform the upgrade with confidence by ensuring our tests pass.

+ +

Before You Start

+ +

Begin by understanding Graphiti. Walk through the documentation and +sample application to get a feel for the new API. Here’s a controller.

+ +

There is no Swagger UI equivalent for Graphiti. Swagger is a +poor fit for graph APIs, and we instead rely on a custom Graphiti +Schema. Instead, check out Vandal.

+ +

Setup

+ +

Start by removing the gems jsonapi_suite, jsonapi-rails, +jsonapi_swagger_helpers, and swagger-diff. You’ll eventually remove +jsonapi_spec_helpers, but keep it for now.

+ +

Add gems graphiti, graphiti_spec_helpers and responders. See the +Sample App Gemfile.

+ +

Move spec/api to spec/legacy.

+ +

Remove config/initializers/strong_resources.rb, +config/initializers/jsonapi.rb, app/controllers/docs_controller.rb, +and the Swagger UI that lives under public.

+ +

Remove swagger helpers from Rakefile.

+ +

Grep for JsonapiErrorable and change to GraphitiErrors.

+ +

Make spec/rails_helper.rb correct (though keep jsonapi_spec_helpers +for now). See sample.

+ +

At this point, running your specs should give you a lot of errors like +"index" not found.

+ +

Upgrading

+ +

Upgrade your ApplicationController.

+ +

Upgrade your ApplicationResource.

+ +
    +
  • Note that this references Rails.application.routes.default_url_options[:host]. That’s set in config/application.rb.
  • +
  • Note the endpoint_namespace. Make this match your current routes.
  • +
  • Reference ApplicationResource.endpoint_namespace in your routes file.
  • +
+ +

Begin rewriting your Resources. Go through spec/payloads and add these +attributes/types to the Resource. Remember to mark these as only: +[:readable], or writable: false, etc. Look at +config/initializers/strong_resources.rb to see if an attribute should +be writable.

+ +

Rewrite custom allow_filters. If it’s a one-liner, just make sure +there is a corresponding attribute. If there is custom logic:

+ +
allow_filter :foo do |scope, value|
+  # ... code ...
+end
+
+# BECOMES
+
+filter :foo, :string, only: [:eq], single: true do
+  eq do |scope, value|
+    # ... code ...
+  end
+end
+ +

Two things about filters: by default, value is now always array. Pass +single: true if your logic only supports a single value. Also: a +filter with a given type now comes with operators - string gets +prefix, suffix, etc. If you don’t support these, limit operations +with only:.

+ +

Note that we now support multiple content types for read requests: +.json and .xml. If you have any clients explicitly putting .json +at the end of the URL, they are now going to get a simple JSON response +instead of JSONAPI. Avoid the responders gem if you don’t want this.

+ +

You may want to move some persistence logic to before_commit.

+ +

Resources now have #base_scope. If you previously were using default_filter or passing in a custom scope in your controller, consider moving to base_scope:

+ +
# app/resources/post_resource.rb
+def base_scope
+  Post.active
+end
+ +

If you have manual sideloading logic with scope, it is highly +recommended you rewriting using params - see relationship docs. If you do still need scope, it now yields the parent ids as the first argument and the actual parent models as the second.

+ +

Finally, you are encouraged to avoid overriding create/update/destroy +directly. Instead, override build, save, and delete, and use +Persistence Lifecycle +Hooks.

+ +

At this point, get all your spec/legacy specs passing.

+ +

When you’re done, generate the new Resource and API Specs. Note that much of this is syntax changes, you can copy/paste large amounts of logic from spec/legacy. To ease this process, try rails g graphiti:api_test PostResource and rails g graphiti:resource_test PostResource.

+ +

You should now have the upgraded and legacy test suite working. We +can now remove the legacy specs:

+ +
    +
  • Remove jsonapi_spec_helpers gem
  • +
  • rm -rf spec/payloads
  • +
  • rm -rf spec/legacy
  • +
+ +

And you’re done! Deploy to a staging environment and verify your API +supports all your real-world scenarios.

+ +

Persistence

+ +

Though you can get specs passing with your existing create, update, +etc, try to rewrite them using hooks. It’s no longer considered a best +practice to override these methods because you’ll be bypassing hooks. +Instead, add hooks and override def save(model) if you need to.

+ +

Gotchas

+ +

We use respond_with in read operations and render jsonapi: in write +operations. This is because the responders gem bypasses renderers for +PUT, and a few other minor issues.

+ +


+

+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/vandal.html b/website/static/1.13/guides/vandal.html new file mode 100644 index 00000000..0d38b37f --- /dev/null +++ b/website/static/1.13/guides/vandal.html @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Vandal

+ +

Vandal is the Graphiti UI. It’s helpful for exploring data, testing and +generating URLs. To take Vandal for a spin, view our sample app (initial load may take a second).

+ +


+

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

+ 1 Installation +

+
+ + + +

+ 1.1 Installing via Template +

+
+ +

If you ran our application template, +you already have Vandal installed. Check your routes to see it mounted.

+ + + +

+ 1.2 Installing via Gem +

+
+ +
    +
  • Add the vandal_ui gem.
  • +
  • Run rake vandal:install
  • +
  • Mount the engine:
  • +
+ +
# config/routes.rb
+scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
+  # ... routes ...
+  mount VandalUi::Engine, at: '/vandal'
+end
+ +

That’s it! Vandal will dynamically generate a schema at <api_namespace>/vandal/schema.json, and you can view the UI at <api_namespace>/vandal.

+ + + +

+ 1.3 Manual Installation +

+
+ +

Vandal is a VueJS +application. Just grab the dist files and put them anywhere you’d like.

+ +

Note that index.html has a placeholder, __SCHEMA_PATH__. Replace +this with a URL hosting your schema, and you’ll be good to go.

+ + + +

+ 2 Usage +

+
+ +

First, make sure your schema is being correctly generated. You should +see Vandal make a request something like /vandal/schema.json - make +sure that looks correct. If it doesn’t, you may need to bounce your +server.

+ +

After selecting an endpoint, use the left rail to configure your +request. Click a relationship once to include it in the response. +If a relationship is included, you can click any row in the table to +view related data.

+ +

Click a relationship twice and you can configure the deep query of +the associated Resource. In other words, if you’re fetching Posts and +Comments, click comments twice to say things like “only active +comments should be returned”.

+ +

When you hit ‘submit’, the top URL bar will change to reflect your query +and results will show in the center table.

+ +


+

+
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/guides/why.html b/website/static/1.13/guides/why.html new file mode 100644 index 00000000..461b21a3 --- /dev/null +++ b/website/static/1.13/guides/why.html @@ -0,0 +1,1121 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ +

+ +

Why Graphiti

+ +

I’ve written GraphQL and had a pleasant experience. I have enormous respect for GraphQL. I also believe there is a fundamental flaw in GraphQL’s design.

+ +

Let’s remember why people like GraphQL in the first place - because it addressed common frustrations with REST APIs:

+ +
+

[We] heard from integrators that our REST API also wasn’t very flexible. It sometimes required two or three separate calls to assemble a complete view of a resource. It seemed like our responses simultaneously sent too much data and didn’t include data that consumers needed.

+ +

- “The Github Graph API”, GitHub Engineering

+
+ +

GraphQL solves real problems. Its flaw is that it solved these +problems using zero-sum thinking: we must +abandon the existing paradigm and forge a new one. It’s GraphQL +versus REST, one or the other. REST is dead, long live GraphQL.

+ +

Graphiti instead approaches the problem using positive-sum thinking:

+ +
+

Positive-sum thinking is how we embrace pluralism while retaining a coherent vision and set of values…A zero-sum view would assume that apparent oppositions are fundamental, e.g., that appealing to the JS crowd inherently hurts the C++ one. A positive-sum view starts by seeing different perspectives and priorities as legitimate and worthwhile, with a faith that by respecting each other in this way, we can find strictly better solutions than had we optimized solely for one perspective.

+ +

- “Listening and Trust”, Aaron Turon

+
+ +

GraphQL optimized around REST’s shortcomings, and in doing so it dropped +REST’s advantages. There is no need for such a zero-sum tradeoff. We can take +everything great about GraphQL and build it on top of REST (and +HTTP!), instead of replacing it altogether. We can have our cake and eat it too.

+ + + +

+ REST +

+
+ +

+ +

+
+ +

You’ll find plenty of GraphQL posts that describe REST as an inflexible paradigm of a bygone era, a lack of granularity that necessarily leads to data under- and over-fetching.

+ +

It’s true that many REST APIs work this way, but this is not REST. While there’s +endless debate around which APIs are considered “RESTful”, I don’t think +we need to look much further than what the letters actually stand for:

+ +
+ +
+

Representational State Transfer. This sentence is not only what REST stands for, it is also the tiniest possible description of what REST actually means…It is not a standard, rather a style describing the act of transfering a state of something by its representation.

+ +

Let’s consider this:

+ +

Marcus is a farmer. He has a ranch with 4 pigs, 12 chickens and 3 cows. He is now simulating a REST API while I am the client. If I want to request the current state of his farm using REST I just ask him: “State?”

+ +

Marcus answers: “4 pigs, 12 chickens, 3 cows”. +This is the most simple example of Representional State Transfer. Marcus transfered the state of his farm to me using a representation. The representation of the farm is the plain sentence: “4 pigs, 12 chickens, 3 cows”.

+ +

So lets get to the next level. How would I tell Marcus to add 2 cows to his farm the REST way? +Maybe tell him: “Marcus, please add 2 cows to your farm”.

+ +

Do you think this was REST? Are we transfering state by its representation here? NO! This was calling a remote procedure. The procedure of adding 2 cows to the farm.

+ +

Marcus sadly answers: “400, Bad Request. What do you mean?”

+ +

So lets try this again. How would we do this the REST way? What was the representation again? It was “4 pigs, 12 chickens, 3 cows”. Ok. so let’s try this again transfering the representation…

+ +

me: “Marcus, … 4 pigs, 12 chickens, 5 cows … please!”. +Marcus: “Alright !”. +me: “Marcus, … what is your state now?”. +Marcus: “4 pigs, 12 chickens, 5 cows”. +me: “Ahh, great!” +See? It was really not that hard and it was REST.

+ +

- “Why REST Is So Important”, Gregor Riegler

+
+ +

In other words, this is REST:

+ +
+

+ +

+
+ +

We’re moving an object from the server to the client, possibly modifying +that object, then moving back to the server.

+ +

The other important aspect of REST is Links. Because all objects are +addressable at a URL, we can use HTTP links to connect Resources. This +allows for (among other things) lazy-loading - the logic connecting Resources can be +changed server-side without breaking clients. If we have a “Top +Comments” relationship, we can redefine “Top” without disturbing +clients.

+ +
+

+ +

+
+ +

Pretty simple right? Here’s how we might implement this in GraphQL:

+ +
type CreateEmployeeInput {
+  name: String
+  age: Int
+}
+
+type CreateEmployeePayload {
+  employee: Employee
+}
+
+type UpdateEmployeeInput {
+  employeeId: ID!
+  name: String
+  age: Int
+}
+
+type UpdateEmployeePayload {
+  employee: Employee
+}
+
+type DestroyEmployeeInput {
+  id: ID!
+}
+
+type DestroyEmployeePayload {
+  employee: Employee
+}
+
+type Employee {
+  id: ID!
+  name: String
+}
+
+createEmployee(input: CreateEmployeeInput!): CreateEmployeePayload
+updateEmployee(input: UpdateEmployeeInput!): UpdateEmployeePayload
+destroyEmployee(input: DestroyEmployeeInput!): DestroyEmployeePayload
+
+employee(id: ID!): Employee
+ +

The first thing to note is that GraphQL is super badass at describing +fields and types. The next thing to note is that fields and types are +the wrong abstraction.

+ +

Defining a schema like this allows bespoke, fine-grained detail. If we +wanted, the CreateEmployeePayload could be different than the +UpdateEmployeePayload - same for inputs like CreateEmployeeInput and +UpdateEmployeeInput. If we wanted other actions, like +promoteEmployee or deactivateEmployee, they would be easy to add and +follow the same basic constructs.

+ +

This is RPC - hand-crafted, custom requests. We have a high level of +configuration but a low level of convention. Not only will +developers have to spend more time hand-crafting these requests, but +patterns are likely to diverge from one API to the next, from team to +team, as time moves on. In fact, the above is really a best-case +scenario with common naming convention of create/update/destroy - the +Github API adds verbs like add, remove, lock, move and more.

+ +

Oh, and we dropped support for lazy-loading along the way.

+ + + +

+ Conventions +

+
+ +

+ +

+ +

The benefit of REST over RPC is conventions. Conventions cause increased +productivity and consistency (leading to fewer misunderstandiings and +chances for bugs). Let’s start thinking in REST, and see where it takes +us.

+ +

In REST, we know the input and output is always the Resource:

+ +
type Employee {
+  id: ID!
+  name: string
+}
+
+createEmployee(input: Employee): Employee
+updateEmployee(input: Employee!): Employee
+destroyEmployee(input: Employee!): Employee
+
+employee(id: ID): Employee
+ +

OK, a little tighter. But there’s actually no reason to type this out +each time - we can assume developers are already familiar with the +convention.

+ +
type Employee {
+  id: ID!
+  name: string
+}
+
+createEmployee
+updateEmployee
+destroyEmployee
+
+employee(id: ID)
+ +

Getting there. OK, and we know we’re dealing with an Employee, and we +know we won’t have custom verbs like promote or remove - we’re +moving objects here, and nothing else (if that throws you for a mental +loop, see this presentation by Derek Prior, Engineering Manager at +GitHub).

+ +
type Employee {
+  id: ID!
+  name: string
+}
+
+create
+update
+destroy
+show // employee(id: ID)
+index // employee()
+ +

By adopting conventions, we not only removed boilerplate - we removed +the chance of subtle inconsistencies. This is better for both providers +and consumers of the API.

+ +

We’re just getting started.

+ +

The above schema covers basic CRUD. But we probably want to filter data, right? Let’s say we want to return all employees with a given name:

+ +
employee(id: ID, name: String)
+ +

Again, we’re seeing chances for inconsistency. What’s the name +parameter - straight equality? Case sensitive? Contains? I guess we +could throw a bunch of suffixes at it:

+ +
employee(id: ID, name_eq: String, name_suffix: String, name_prefix: String, name_contains: String, name_not_eq: String, name_not_suffix: String, name_prefix: String, name_not_prefix: String)
+ +

Works, but a bit unwieldy…and again, likely to diverge wildly across +implementations.

+ +

What about sorting? Should we do it similar to the Github API?:

+ +
enum OrderDirection {
+  ASC
+  DESC
+}
+
+enum EmployeeOrderField {
+  ID
+  NAME
+}
+
+type EmployeeOrder {
+  field: EmployeeOrderField!
+  direction: OrderDirection
+}
+
+employee(orderBy: EmployeeOrder)
+ +

Or should we do it like How to +GraphQL?:

+ +
enum EmployeeOrderByInput {
+  id_ASC
+  id_DESC
+  name_ASC
+  name_DESC
+}
+
+employee(orderBy: EmployeeOrderByInput)
+ +

We have divergent APIs right off the bat, and neither one supports +multisort.

+ +

Rethinking REST

+ +

+ +

+
+ +

Earlier, we covered RESTfully moving an object between the server and +the client. Those objects connected together with Links, which allowed +for lazy-loading.

+ +

Instead of throwing away this paradigm, what if we just added +eager-loading?

+ +
+

+ +

+
+ +

REST doesn’t have a query +specification or built-in schema, but it does have this Resource concept. Instead of +thinking of a bag of fields and types, what if we thought in Resources?:

+ +

Resources have Attributes. An Employee has a first_name which is +a string, an age which is an integer, and so forth.

+ +

We’d probably want to filter and sort by these attributes right? We might add +some additional filters and sorts, we might want to opt-out of others, +but querying a Resource by its attributes serves as a reasonable +baseline.

+ +

If we have an attribute and it’s a string, we know we’re +talking about operators like suffix and prefix, but an integer +attribute would want operators like greater_than and less_than.

+ +

Resources also have relationships to other Resources. We should be +able to lazy-load those relationships (with Links), or eager-load +those relationships. Whether lazy or eager, the same logic should apply.

+ +

If we defined these Resources, with their attributes and relationships, +then the input and the output wouldn’t actually matter. Or more +accurately: we can accept and render whatever payload we want. Maybe +we’ll even dynamically serve different payloads based on Content-Type.

+ +

OK, so really we don’t need to define inputs and outputs - those can +be assumed by convention, swapped on-demand. What we really need to define is the Resource.

+ + + +

+ Welcome to Graphiti +

+
+ +
class EmployeeResource < ApplicationResource
+  attribute :name, :string, sortable: true, filterable: true
+  attribute :age, :integer, writable: false
+end
+ +

With nothing but this Resource definition and some assumed conventions, +we get all this behavior out of the box:

+ +
    +
  • Create
  • +
  • Update
  • +
  • Delete
  • +
  • Read +
      +
    • Filter +
        +
      • String (name) +
          +
        • eq (case sensitive)
        • +
        • eql (case insensitive)
        • +
        • prefix
        • +
        • suffix
        • +
        • match
        • +
        • not_* (not_eq, not_prefix, etc)
        • +
        +
      • +
      • Dates and Numbers (age) +
          +
        • eq
        • +
        • gt (greater than)
        • +
        • lt (less than)
        • +
        • gte (greater than/equal to)
        • +
        • lte (less than/equal to)
        • +
        +
      • +
      +
    • +
    • Sort / Multisort
    • +
    • Paginate
    • +
    • Fieldsets
    • +
    +
  • +
+ +

Not just an API contract, but out-of-the-📦 behavior. If we know +the configuration of a Resource, we can automate query and persistence +operations. Obviously we’ll need lots of ways to customize and +override, and support for any number of datastores and clients. But if +we have the configuration, a defined contract for querying and +persisting data, we can build patterns around it.

+ +

There’s more to this than a bunch of out-of-the-📦 standards and +behavior. If we thought only in Fields and Types, we’d use GraphiQL to +see something like:

+ +


+ +

+ +

+ +


+ +

But if we thought in Resources, it’s not just a big bag of fields. We’re +able to organize those fields around meaningful concepts:

+ +


+ +

+ +

+ +


+ +

This screenshot is from Vandal, the Graphiti UI.

+ +

Because we started with a better abstraction, we ended with a more +intuitive +UI. As a marketer-turned-programmer myself, I really +appreciate when data exploration tools like this are friendly to +less-technical users. A user of Vandal doesn’t need to know about Connections +or Edges, they just need to click around. I like that my product owner and I can walk +through the domain together, validating concepts and solidifying a shared +understanding.

+ + + +

+ HTTP Endpoints +

+
+ +

While both GraphQL and Graphiti can fetch your graph of data in a single +request, GraphQL does this with a single endpoint. Graphiti keeps +RESTful URLs, for three reasons. First, HTTP caching: Phil Sturgeon has +an excellent comparison of GraphQL and endpoint-based caching here.

+ +

Second, adding endpoints allows for customization. Maybe we want to send +a welcome email when we POST /employees but not when we POST +/admin/employees. Maybe /exemplary_employees applies additional query +logic by default, but wants to re-use everything else. In other words, +Resources form your graph, but Endpoints expose that graph to the +outside world (and hold relevant logic around exposure). Read more in +our Endpoints Guide.

+ +

The third reason is lazy-loading with Links, which we’ll cover in a bit.

+ + + +

+ Schema +

+
+ +

We even get schema benefits. Schemas are great for tooling and +backwards-compatibility checks…but when they are oriented around +Fields and Types, they can only tell you so much. When they are oriented +around Resources, they can expose less-obvious concepts. Maybe we sort +Employees by created_at by default:

+ +
{
+  name: "EmployeeResource",
+  type: "employees",
+  attributes: { ... },
+  default_sort: [{ "created_at": "desc" }],
+  ...
+}
+ +

Because this is specified in the schema, not only are clients more +informed, but changing this default would raise a backwards-compatibility +error:

+ +
EmployeeResource: default sort changed from [{:created_at=>"desc"}] to [{:last_name=>"asc"}].
+ +

When developing in Graphiti, we introspect your Resources and +automatically generate the schema for you. Backwards-compatibility +checks can be done with a command-line task, or whenever your tests run.

+ + + +

+ Graphs +

+
+ +

You may be thinking, “OK, but REST only works for a single object. I’ll +have to make multiple requests, and be right back where I started. I +need GraphQL to solve this problem”.

+ +

Not true.

+ +

Years before GraphQL came out, respected developers from different +companies and backgrounds came together and began the discussion on how to improve REST +APIs. This wasn’t a project pushed by a hundred-billion dollar +company; it was an organic, community-driven effort. The result was the JSON:API standard, +which tackled granular queries long ago:

+ +
# GET /employees?include=positions
+
+{
+  data: {
+    id: "123",
+    type: "employees",
+    attributes: { name: "Kayla Webb" },
+    relationships: {
+      positions: {
+        data: { id: "456", type: "positions" }
+      }
+    }
+  },
+  included: [
+    {
+      id: "456",
+      type: "positions",
+      attributes: { title: "Engineer" }
+    }
+  ]
+}
+ +

Wonky payload, right? First of all, don’t be scared. In Graphiti, +you can add .json to the URL and output a more traditional flat +structure:

+ +
{
+  data: {
+    id: "123",
+    name: "Jeesoo Ryoo",
+    positions: [{ id: "456", title: "Engineer" }]
+  }
+}
+ +

You can absolutely develop in Graphiti this way, but you’d be giving up +some smart things JSON:API does. One example is de-duplicating each node +in the graph: if we’re listing 100 Posts and they all have the same +Author, you’ll have to render that Author 100 times. JSON:API would +only render it once.

+ +

Another is the type/id combo:

+ +

+

+ + +
+

+ +

There’s a bunch of reasons this is important, but one I’ve always been +partial to is websockets. This type/id combo allows us to uniquely +identify records once they’ve been loaded in JS memory. So, every time a +Resource is saved we can push its state (JSON representation) to clients +with a websocket. Here I am randomly updating a bunch of backend data, +and watching the UI update in real-time.

+ +

+ +

+


+ +

This took only a handful of lines of code, all of which could be +packaged into a library to make this automatic.

+ +

Another example is lazy-loading data with Links:

+ +

+

+ + +
+

+ +

Instead of loading everything up-front, we want to defer loading for +performance reasons. Maybe we want to render our Employee detail page +super quick, and we don’t need to list the Positions until the user +clicks something.

+ +

You can do this in GraphQL, but you need to bake logic into the client, +which means changing the logic would break clients (read more about +this in the Links Guide). +Luckily, REST and JSON:API are optimized for lazy-loading:

+ +
# ...
+positions: {
+  links: {
+    related: 'http://example.com/api/positions?filter[employee_id]=123'
+  }
+}
+# ...
+ +

Graphiti generates these Links between Resources automatically. If you +change the logic connecting Resources (which applies to eager loading as +well), we’ll update the Link - clients can simply follow the +link, and we can change logic server-side with no breakage.

+ +

There’s a bunch of other great stuff about JSON:API, but that is a +topic for another day.

+ +
+

(Quick aside: I’ve always found the name of this project hilariously +awkward, easy to confuse with any API outputting JSON. But you could say +the same about GraphQL! Just as JSON:API isn’t the only API standard +outputting JSON, GraphQL isn’t the only Graph Query Language - in fact, +you could call JSON:API a Graph Query Language as well!)

+
+ + + +

+ REST != Multiple Requests +

+
+ +

Anything you could do with a single Resource, you can do with multiple +Resources. In other words, we can fetch an Employee, and their +Positions - but only active positions where the title starts with +Eng, ordered by created_at. This is called Deep Querying.

+ +

The point here is that REST doesn’t mean multiple requests. Sure, our +state is now a graph of objects instead of a single object, but there’s +no need for a wholesale revamp. In fact, we don’t need to change much at +all.

+ +

+ +

+ +

Just as we can query multiple Resources at once, we can also +persist multiple objects at once.

+ +

When persisting in REST, we send the same object back to the server +alongside a verb. That verb tells us if we’re creating, updating (part +or whole), or deleting. Same thing here. Let’s specify the verb +alongside the relationship:

+ +
# ...
+positions: {
+  data: { id: "456", type: "positions", method: "destroy" }
+}
+# ...
+ +

There are only 4 possible verbs:

+ +
    +
  • create
  • +
  • update
  • +
  • destroy
  • +
  • disassociate
  • +
+ +

Oh, and we’ll run everything within a transaction, throwing in +conventions for data validation as well:

+ +
# Response code 422
+
+{
+  code:  'unprocessable_entity',
+  status: '422',
+  title: "Validation Error",
+  detail: "Name can't be blank",
+  source: { pointer: '/data/attributes/name' },
+  meta: {
+    attribute: :name,
+    message: "can't be blank",
+    code: :blank
+  }
+}
+ +

Earlier, we covered how REST was optimized for lazy-loading. Graphiti +builds on top of REST to add eager-loading, and “eager-persisting”. In +other words, we can both read and write a graph of data in a single +request.

+ + + +

+ (Micro) Services +

+
+ +
+

You should think hard before breaking up a Majestic Monolith; beware the tradeoffs. Still, if you need it, Graphiti has your back.

+
+ +

We now have a consistent interface for queries and relationships. We +also covered how Resources connect together with Links. Put two and +two together, and you’ll see a Resource doesn’t need to be local to same +application. We can have cross-API, remote Resources as well.

+ +

The popular GraphQL platform Apollo does something similar with Schema Stitching:

+ +
+

+ +

+
+ +

You won’t need to write code like this in Graphiti. Because we have +conventions, we can automate this stuff. Just supply a URL:

+ +
class EmployeeResource < ApplicationResource
+  has_many :positions, remote: 'http://example.com/api/positions'
+end
+ +

That’s it. Everything works the same. We can fetch an Employee and her Positions in a single +request, add additional local or remote Resources to the request, and +Deep Query. If you use Vandal, you’ll think it’s all the same API.

+ +

Microservices 🎉!

+ + + +

+ Don't Sleep On This One: Domain-Driven Design +

+
+ +
+

+ +

+
+
+
+ +

OK, not that DDD. This DDD:

+ +
+

+ +

+
+
+ +

This book can be a little dry, but the concepts are excellent. Let’s +check StackOverflow for a quick summary of DDD:

+ +
+ +
+

DDD is about trying to make your software a model of a real-world system or process. In using DDD, you are meant to work closely with a domain expert who can explain how the real-world system works. For example, if you’re developing a system that handles the placing of bets on horse races, your domain expert might be an experienced bookmaker.

+ +

Between yourself and the domain expert, you build a ubiquitous language (UL), which is basically a conceptual description of the system. The idea is that you should be able to write down what the system does in a way that the domain expert can read it and verify that it is correct. In our betting example, the ubiquitous language would include the definition of words such as ‘race’, ‘bet’, ‘odds’ and so on.

+ +

The concepts described by the UL will form the basis of your object-oriented design. DDD provides some clear guidance…recommends several patterns…

+ +

- Rob Knight

+
+ +

I trim the quote because the prescriptive patterns of DDD often +overshadow its core concepts. The creator of DDD, Eric Evans covered this in his +keynote last year.

+ +

REST and Resources force a developer to think deeply about their +domain, and this leads to better object-oriented code. +Rather than a bag of fields and types, Graphiti pushes you to think +critically about your domain.

+ +

A Resource is what DDD calls an Entity. REST is about moving Domain Entities from client to server and back again.

+ +

You don’t need to know anything about DDD to develop in Graphiti. +But don’t sleep on it.

+ + + +

+ Clients +

+
+ +

You can use any HTTP client with Graphiti. If you’re using JS, a simple +fetch will do.

+ +

But you may be looking for a more robust client, one that takes +advantage of Graphiti conventions. Look no further than +Spraypaint, our +official client heavily inspired by ActiveRecord. Query your API the +same way you query your database:

+ +
// All of this is chainable
+
+let { data } = await Employee
+  .where({ name: { prefix: "Jane" } })
+  .order({ created_at: "desc" })
+  .page(2).per(10)
+  .select(['name', 'age'])
+  .stat({ total: 'count' })
+  .includes({ positions: 'department' })
+  .all()
+
+let record = data[0]
+record.positions[0].department.name = 'Updated!'
+await record.save({ with: { positions: 'department' } })
+ +

By relying on conventions we can move the URL, request, and response +under the hood, allowing you to focus on what matters - your domain.

+ + + +

+ GraphQL Support +

+
+ +

OK, let’s come full circle. Let’s say some of these conventions resonate +with you, but nevertheless you’d like to develop a GraphQL API. I still think +Graphiti is your best bet, because Graphiti supports GraphQL.

+ +

Remember, we started with this long-hand RPC code:

+ +
type CreateEmployeeInput {
+  name: String
+  age: Int
+}
+
+type CreateEmployeePayload {
+  employee: Employee
+}
+
+type UpdateEmployeeInput {
+  employeeId: ID!
+  name: String
+  age: Int
+}
+
+type UpdateEmployeePayload {
+  employee: Employee
+}
+
+type DestroyEmployeeInput {
+  id: ID!
+}
+
+type DestroyEmployeePayload {
+  employee: Employee
+}
+
+type Employee {
+  id: ID!
+  name: String
+}
+
+createEmployee(input: CreateEmployeeInput!): CreateEmployeePayload
+updateEmployee(input: UpdateEmployeeInput!): UpdateEmployeePayload
+destroyEmployee(input: DestroyEmployeeInput!): DestroyEmployeePayload
+
+employee(id: ID!): Employee
+ +

Graphiti does not need all this boilerplate. But +if we have the short-hand, that means we can automatically generate +the long-hand. We can introspect the Graphiti Resources, and use +graphql-ruby to automatically generate GraphQL code.

+ +

That project is graphiti-graphql. While still more of an experiment at this stage, it’s shaping up nicely. The main blockers are the conventions missing from any GraphQL API - how should we render validation errors, which sorting standard should we adopt, etc. But we have proof that if there’s a target to hit, we can autogenerate it.

+ +

So we can generate GraphQL via Graphiti, but should we? Maybe! Not +an unreasonable pursuit. If you’d like to go down this route, I’d love +to hear from you!

+ +

Still, let’s be clear what we’re missing: HTTP caching, error codes, lazy-loading, and more.

+ + + +

+ Magic +

+
+ +
+

Having that level of consistency, and working with that for +just a little while means that you can start to forget about it. And +that’s the power of conventions in general…it used to be something +you had to think about and make a decision. Well, decisions are bad. +Decisions take up your brain power, and it requires brain cycles to +consider which or the other. The more decisions you can take out of +the whole thing, the more brain power you can free up to consider the +really important things.

+ +

If everybody is doing the same thing in the same way, it means that +you can easily go from one application to the other, and expect the +same things to happen.

+ +

- “Resources on Rails”, David Heinemeier Hansson

+
+ +

When conventions form your programming foundation, you end up with these +high-level abstractions where a few lines of code hide the underlying +complexity. Often, this is flippantly referred to as ✨”Magic“🔮

+ +

The thing about magic is, once you learn the trick it often comes down +to something simple: a mirror, a trick deck, a quick hand.

+ +

An object, moving back and forth.

+ +

+ +

+ +

Simple concepts can often be the most powerful, and I will never get +tired of seeing this trick performed.

+ +

There’s plenty more we haven’t even touched on. Check out the +Guides, or dive right in with the Quickstart or Tutorial. If +things get tricky, ask for help in our Discord Chat. Reach out to me +directly at richmolj@gmail.com or @richmolj on Twitter.

+ + + +

+ Addendum: Holy Shit +

+
+ +

I can’t believe I just wrote all that. I swear, I never set out to do any of this.

+ +

There are a lot of big ideas here, but this project is not some ideological crusade. Three years ago I had a bug in my application, and I found it surprisingly tricky to fix. I kept pulling that string, I connected with other developers pulling their own strings, one thing led to another, and here we are. This project is nothing more than a good-faith effort to build quality software, based on my own personal experience.

+ +

Let’s be clear: GraphQL is awesome. It solves real problems in a +user-friendly manner, has sexy tooling and amazing libraries created +by kickass developers. There are many advantages of GraphQL not covered +here, and plenty more great ideas I’d like to steal. If you find some of +the framing here annoying, if you find yourself thinking “…but what +about X and Y?”, just know I feel the same way whenever a GraphQL +post talks about REST. It will be a continual goal of this project to +raise the level of debate.

+ +

Same for RPC. I work for a company that made billions off of RPC. I tend +to think REST is a better fit for the web, and RPC is a better fit for +things like financial trading systems or video games, but plenty of +reasonable people can disagree. Conventions and abstractions come with +their own cost: a learning curve, indirection, lack of flexibility. If +you prefer something more low-level, I completely understand. I’ll +continue trying to learn from you.

+ +

Beware projects and ideologies that claim to be a solution to all your +problems. Everything has tradeoffs. The best we can do is to avoid +zero-sum thinking, and instead think about how we can take the best +ideas from a variety of solutions and experiences, striving to +continually improve. It is hard, it is exhausting, it is the right thing +to do.

+ +

Unless you like XML. Screw XML.

+ +


+ +

+ +

+ +
+
+
+ + + + + + diff --git a/website/static/1.13/index.html b/website/static/1.13/index.html new file mode 100644 index 00000000..f9e1ea01 --- /dev/null +++ b/website/static/1.13/index.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+ + +

Stylish Graph APIs

+
+ + + +

+ RESTful APIs for a GraphQL era. + + But Why? +

+
+
+ +
+
+ + diff --git a/website/static/1.13/js/authentication.html b/website/static/1.13/js/authentication.html new file mode 100644 index 00000000..d71c03df --- /dev/null +++ b/website/static/1.13/js/authentication.html @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Authentication

+ +

Spraypaint supports JSON Web Tokens. These can +be set manually, or automatically fetched from localStorage.

+ +

To set manually:

+ +
ApplicationRecord.jwt = 'myt0k3n'
+ +
+

All requests will now send the header:
+Authorization: Token token="myt0k3n".

+
+ +

To set via localStorage, simply store the token with a key of jwt +and it will be set automatically. To customize the localStorage key:

+ +
ApplicationRecord.jwtStorage = "authtoken"
+ +

…or to opt-out of localStorage altogether:

+ +
ApplicationRecord.jwtStorage = false
+ +

You can control the format of the header that is sent to the +server:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  class ApplicationRecord extends SpraypaintBase {
+    // ... code ...
+    static generateAuthHeader(token) {
+      return `Bearer ${token}`
+    }
+  }
+  
+ +
  var ApplicationRecord = SpraypaintBase.extend({
+    // ... code ...
+    static: {
+      generateAuthHeader: function(token) {
+        return "Bearer " + token;
+      }
+    }
+  });
+  
+ +
+ +

Finally, if your server returns a refreshed JWT within the X-JWT +header, it will be used in all subsequent requests (and localStorage +will be updated automatically if you’re using it).

+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/ddau.html b/website/static/1.13/js/ddau.html new file mode 100644 index 00000000..bd9885be --- /dev/null +++ b/website/static/1.13/js/ddau.html @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Data Down, Actions Up

+ +

It’s a popular pattern to pass data down to components, avoid modifying state within the component, and instead pass actions up to modify state. This can make complex applications easier to track and reason about, and you’ll see it in client-side frameworks from React to Ember.

+ +

To follow this pattern, use #dup() when passing down to your component:

+ +
<my-component something="model.dup()" />
+ +

This will create a new instance of the model with all the same state. +Avoid modifying this instance in your component and instead pass +actions up.

+ +

When opting-in to state-syncing these instances will sync-up whenever one of these is instances is persisted. You won’t have to worry about updating the child component when the parent instance is saved.

+
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/extra-params.html b/website/static/1.13/js/extra-params.html new file mode 100644 index 00000000..c62bc4f0 --- /dev/null +++ b/website/static/1.13/js/extra-params.html @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Extra Params

+ +

Sometimes you need to submit params that are not standard jsonapi params. One great example would be +https://yourdomain.com/users?debug=true which is not a param for the UserResource you may have, but +might enable functionality in your controller as needed.

+ +

Invoking it is pretty straightforward, just invoke extraParams and pass in params and values you wish +to add to your API call when executed.

+ +
YourRecord.extraParams({ debug: true })
+ +

One common way to use this globally is to put this into a base class so it can be chained as part of +every resource.

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  @Model
+  export class ApplicationRecord extends SpraypaintBase {
+    static withDebug<T extends ApplicationRecord>(): Scope<T> {
+      return this.extraParams({ debug: true }) as Scope<T>;
+    }
+  }
+  // unfortunately you will need to pass in the 
+  // implementing class' type as a generic
+  UserRecord.withDebug<UserRecord>().all()
+  
+ +
  const ApplicationRecord = SpraypaintBase.extend({
+    static: {
+      withDebug: () => this.extraParams({ debug: true });
+    }
+  })
+  UserRecord.withDebug().all()
+  
+ +
+
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/index.html b/website/static/1.13/js/index.html new file mode 100644 index 00000000..8767f0af --- /dev/null +++ b/website/static/1.13/js/index.html @@ -0,0 +1,321 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+
+

Start here: Why Spraypaint?

+
+ +
+
+ Typescript +
+
+ Javascript +
+
+
+ +
// Spraypaint is like "ActiveRecord in Javascript". It can:
+//
+// * Deeply nest reads and writes
+// * Automatically handle validation errors
+// * Replace *ux patterns
+// * ...and much more!
+
+// define models
+@Model()
+class ApplicationRecord extends SpraypaintBase {
+  static baseUrl = "http://my-api.com"
+  static apiNamespace = "/api/v1"
+}
+
+@Model()
+class Person extends ApplicationRecord {
+  static jsonapiType = "people"
+
+  @Attr() firstName: string
+  @Attr() lastName: string
+
+  get fullName() {
+    return `${this.firstName} ${this.lastName}`
+  }
+}
+
+// execute queries
+Person
+  .where({ first_name: 'John' })
+  .order({ created_at: 'desc' })
+  .per(10).page(2)
+  .includes({ jobs: 'company' })
+  .select({ people: ['first_name', 'last_name'] })
+
+// persist data
+let person = new Person({ firstName: 'Jane' })
+person.save()
+    
+ +
// Spraypaint is like "ActiveRecord in Javascript". It can:
+//
+// * Deeply nest reads and writes
+// * Automatically handle validation errors
+// * Replace *ux patterns
+// * ...and much more!
+
+var spnt = require('spraypaint/dist/spraypaint')
+
+// define models
+const ApplicationRecord = spnt.JSORMBase.extend({
+  static: {
+    baseUrl: 'http://my-api.com',
+    apiNamespace: '/api/v1'
+  }
+})
+
+const Person = ApplicationRecord.extend({
+  attrs: {
+    firstName: spnt.attr(),
+    lastName: spnt.attr()
+  },
+  methods: {
+    fullName: function() {
+      return this.firstName + ' ' + this.lastName;
+    }
+  }
+})
+
+// execute queries
+Person
+  .where({ first_name: 'John' })
+  .order({ created_at: 'desc' })
+  .per(10).page(2)
+  .includes({ jobs: 'company' })
+  .select({ people: ['first_name', 'last_name'] })
+
+// persist data
+var person = new Person({ firstName: 'Jane' })
+person.save()
+    
+ +
+
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/installation.html b/website/static/1.13/js/installation.html new file mode 100644 index 00000000..e880e35e --- /dev/null +++ b/website/static/1.13/js/installation.html @@ -0,0 +1,637 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Installation

+ +

Installation is straightforward. Since we use fetch underneath the +hood, we recommend installing alongside a fetch polyfill.

+ +

If using yarn:

+ +
$ yarn add spraypaint isomorphic-fetch
+ +

If using npm:

+ +
$ npm install spraypaint isomorphic-fetch
+ +

Now import it:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
import {
+  Model,
+  SpraypaintBase,
+  Attr,
+  BelongsTo,
+  HasMany
+  // etc
+} from "spraypaint"
+ +
const {
+  SpraypaintBase,
+  attr,
+  belongsTo,
+  hasMany
+  // etc
+} = require("spraypaint/dist/spraypaint")
+ +
+ +

…or, if you’re avoiding JS modules, spraypaint will be available as a global in +the browser.

+ +

Typescript

+ +

Spraypaint supports Typescript versions >= 2.8.

+ +

By default you will need a ! after each attribute and relationship:

+ +
@Attr first_name!: string
+@HasMany() positions!: Position[]
+ +

This is due to Strict Class Initialization. For the purposes of Spraypaint, we don’t need this. Remove the need for +! (as the rest of these guides do) by setting

+ +

"strictPropertyInitialization": false

+ +

in tsconfig.json.

+ +

Defining Models

+ +

Connecting to the API

+ +

Just like ActiveRecord, our models will inherit from a base class that +holds connection information (ApplicationRecord, or +ActiveRecord::Base in Rails < 5):

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
@Model()
+class ApplicationRecord extends SpraypaintBase {
+  static baseUrl = "http://my-api.com"
+  static apiNamespace = "/api/v1"
+}
+ +
const ApplicationRecord = SpraypaintBase.extend({
+  static: {
+    baseUrl: "http://my-api.com",
+    apiNamespace: "/api/v1"
+  }
+})
+ +

All URLs follow the following pattern:

+ +
    +
  • baseUrl + apiNamespace + jsonapiType
  • +
+ +

As you can see above, typically baseUrl and apiNamespace are set on +a top-level ApplicationRecord (though any subclass can override). +jsonapiType, however, is set per-model:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
@Model()
+class Person extends ApplicationRecord {
+  static jsonapiType = "people"
+}
+ +
const Person = ApplicationRecord.extend({
+  static: {
+    jsonapiType: "people"
+  }
+})
+ +
+ +

With the above configuration, all Person endpoints will begin +http://my-api.com/api/v1/people.

+ +
+

TIP: Avoid CORS and use relative paths by simply setting baseUrl to +""

+
+ +
+

TIP: You can always use the endpoint option to override this pattern +and set the endpoint manually.

+
+ +
Setting Application Name
+ +

It can be helpful to send the name of your client application in request +headers. With this information, servers can keep track of which clients +are hitting which APIs.

+ +

To do this:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
@Model()
+class Person extends ApplicationRecord {
+  static clientApplication = "sales-backend"
+}
+ +
const Person = ApplicationRecord.extend({
+  static: {
+    clientApplication: "sales-backend"
+  }
+})
+ +
+ +

Defining Attributes

+ +

ActiveRecord automatically sets attributes by introspecting database +columns. We could do the same - swagger.json is our schema - but tend +to agree with those who feel this aspect of ActiveRecord is a bit too +“magical”. In addition, explicitly defining our attributes can be used +to track which applications are using which attributes of the API.

+ +

Though this is configurable, by default we expect the API to be +under_scored and attributes to be camelCased.

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
@Model()
+class Person extends ApplicationRecord {
+  // ... code ...
+  @Attr() firstName: string
+  @Attr() lastName: string
+  @Attr() age: number
+
+  get fullName() : string {
+    return `${this.firstName} ${this.lastName}`
+  }
+}
+
+let person = new Person({ firstName: "John" })
+person.firstName // "John"
+person.lastName = "Doe"
+person.attributes // { firstName: "John", lastName: "Doe" }
+person.fullName // "John Doe"
+ +
const attr = spraypaint.attr
+const Person = ApplicationRecord.extend({
+  // ... code ...
+  attrs: {
+    firstName: attr(),
+    lastName: attr(),
+    age: attr()
+  },
+  methods: {
+    fullName: function() {
+      return this.firstName + " " + this.lastName;
+    }
+  }
+})
+
+var person = new Person({ firstName: "John" })
+person.firstName // "John"
+person.lastName = "Doe"
+person.attributes // { firstName: "John", lastName: "Doe" }
+person.fullName() // "John Doe"
+ +
+ +

Attributes can be marked read-only, so they are never sent to the server +on a write request:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
@Attr({ persist: false }) createdAt: string
+@Attr({ persist: false }) updatedAt: string
+ +
attrs: {
+  createdAt: attr({ persist: false }),
+  updatedAt: attr({ persist: false })
+}
+ +
+ +

Defining Relationships

+ +

Just like ActiveRecord, there are HasMany, BelongsTo, and +HasOne relationships:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
@Model()
+class Dog extends ApplicationRecord {
+  // ... code ...
+  @BelongsTo() person: Person[]
+}
+
+class Person extends ApplicationRecord {
+  // ... code ...
+  @HasMany() dogs: Dog[]
+}
+ +
const hasMany = spraypaint.hasMany
+const belongsTo = spraypaint.belongsTo
+
+const Person = ApplicationRecord.extend({
+  // ... code ...
+  attrs: {
+    dogs: hasMany()
+  }
+})
+
+const Dog = ApplicationRecord.extend({
+  // ... code ...
+  attrs: {
+    person: belongsTo()
+  }
+})
+ +
+ +

By default, we expect the relationship name to correspond to a +pluralized jsonapiType on a separate Model. If your models don’t +use this convention, feel free to supply it explicitly:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
@Model()
+class Dog extends ApplicationRecord {
+  // ... code ...
+  @BelongsTo('people') owner: Person[]
+}
+
+// alternatively, specify the class directly
+
+class Dog extends ApplicationRecord {
+  // ... code ...
+  @BelongsTo(Person) owner: Person[]
+}
+ +
const Dog = ApplicationRecord.extend({
+  // ... code ...
+  attrs: {
+    owner: belongsTo('people')
+  }
+})
+ +
+ +

Relationships can be:

+ +
    +
  • Assigned via constructor
  • +
  • Assigned directly
  • +
  • Automatically loaded via .includes() (see reads)
  • +
  • Saved in a single request .save({ with: 'dogs' }) (see +writes)
  • +
+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
let dog = new Dog({ name: "Fido" })
+let person = new Person({ dogs: [dog] })
+person.dogs[0].name // "Fido"
+
+let person = new Person()
+person.dogs = [dog]
+person.dogs[0].name // "Fido"
+
+// Will auto-create Dog instance
+let person = new Person({ dogs: [{ name: "Scooby" }] })
+person.dogs[0].name // "Scooby"
+
+let person = (await Person.includes('dogs')).data
+person.dogs // array of Dog instances from the server
+  
+ +
var dog = new Dog({ name: "Fido" })
+var person = new Person({ dogs: [dog] })
+person.dogs[0].name // "Fido"
+
+let person = new Person()
+person.dogs = [dog]
+person.dogs[0].name // "Fido"
+
+// Will auto-create Dog instance
+var person = new Person({ dogs: [{ name: "Scooby" }] })
+person.dogs[0].name // "Scooby"
+
+Person.includes('dogs').then((response) => {
+  var person = response.data
+  person.dogs // array of Dog instances from the server
+})
+  
+ +
+ + +
+
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/introduction.html b/website/static/1.13/js/introduction.html new file mode 100644 index 00000000..8d16fa5f --- /dev/null +++ b/website/static/1.13/js/introduction.html @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Why Spraypaint?

+ +

Contracts like JSONAPI and GraphQL treat the API like a database. When querying a +database, we have two options:

+ +
    +
  • Type the low-level query language directly (in the database world, +this would be hand-typing SQL).
  • +
  • Use an ORM (like Rails’s ActiveRecord, Phoenix’s Ecto, Django’s +DjangoORM, or Node’s Sequelize).
  • +
+ +

While both options have pros and cons, we tend to think ORMs +have two overwhelming benefits: ease of use and composable queries. +We’ll explore both these concepts in other sections.

+ +

So, we want a javascript ORM for our JSONAPI “database”. Because +ActiveRecord is arguably the most well-known ORM, we’ve tried to match +its interface to make this library accessible to new users. That said, +you’ll find we’ve tried to favor explicitness over implicitness in +order to avoid common ActiveRecord pitfalls.

+ +
+ + + +
+
+
+ + + + + + diff --git a/website/static/1.13/js/middleware.html b/website/static/1.13/js/middleware.html new file mode 100644 index 00000000..2a6027ba --- /dev/null +++ b/website/static/1.13/js/middleware.html @@ -0,0 +1,318 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Middleware

+ +

Middleware is helpful whenever you want to globally intercept request. +This is accomplished by assigning a MiddlewareStack to your +ApplicationRecord. Each stack has beforeFilters and afterFilters +where you can globally modify requests. If you throw("abort"), the +promise will be rejected.

+ +

Example: redirecting to the login page every time the server returns 401:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  import { MiddlewareStack } from 'spraypaint'
+
+  let middleware = new MiddlewareStack()
+  middleware.afterFilters.push((response, json) => {
+    if (response.status === 401) {
+      window.location.href = "/login"
+      throw("abort")
+    }
+  })
+
+  ApplicationRecord.middlewareStack = middleware
+  
+ +
  var MiddlewareStack = spraypaint.MiddlewareStack;
+
+  var middleware = new MiddlewareStack();
+  middleware.afterFilters.push(function(response, json) {
+    if (response.status === 401) {
+      window.location.href = "/login";
+      throw("abort");
+    }
+  });
+
+  ApplicationRecord.middlewareStack = middleware;
+  
+ +
+ +

Example: adding a custom header before the request is sent:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  import { MiddlewareStack } from 'spraypaint'
+
+  let middleware = new MiddlewareStack()
+  middleware.beforeFilters.push((url, options) => {
+    options.headers["CUSTOM-HEADER"] = "whatever"
+  })
+
+  ApplicationRecord.middlewareStack = middleware
+  
+ +
  var MiddlewareStack = spraypaint.MiddlewareStack;
+
+  var middleware = new MiddlewareStack();
+  middleware.beforeFilters.push(function(url, options) {
+    options.headers["CUSTOM-HEADER"] = "whatever";
+  });
+
+  ApplicationRecord.middlewareStack = middleware;
+  
+ +
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/reads/fieldsets.html b/website/static/1.13/js/reads/fieldsets.html new file mode 100644 index 00000000..c70e478a --- /dev/null +++ b/website/static/1.13/js/reads/fieldsets.html @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Sparse Fieldsets

+ +

Use #select() to limit the fields returned by the server:

+ +
Post.select(['title', 'status']).all()
+ +
+

/posts?fields[posts]=title,status

+
+ +

When dealing with relationships, it may be easier to pass an object, +where the key is the corresponding JSONAPI type. This will be exactly +what’s sent to the server in ?fields:

+ +
Post.select({
+  posts: ['title', 'status'],
+  comments: ['created_at']
+}).all()
+ +
+

/posts?fields[posts]=title,status&fields[comments]=created_at

+
+ +

Extra Fieldsets

+ +

Use #selectExtra() to explicitly request a field that doesn’t usually +come back (often computationally expensive):

+ +
Post.selectExtra(['highlights', 'cumulative_ranking']).all()
+ +
+

/posts?extra_fields[posts]=highlights,cumulative_ranking

+
+ +

Just like the select example above, feel free to pass an object +specifying the fields for each relationship.

+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/reads/filtering.html b/website/static/1.13/js/reads/filtering.html new file mode 100644 index 00000000..ce405819 --- /dev/null +++ b/website/static/1.13/js/reads/filtering.html @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Filtering

+ +

Use #where() to apply filters:

+ +
Post.where({ important: true }).all()
+ +
+

/posts?filter[important]=true

+
+ +

#where() clauses can be chained together. If the same key is seen +twice, it will be overridden:

+ +
Post
+  .where({ important: true })
+  .where({ ranking: 10 })
+  .where({ important: false })
+  .all()
+ +
+

/posts?filter[important]=false&filter[ranking]=10

+
+ +

#where() clauses are based on server implementation. The key +should be exactly as the server understands it. Here are some common +conventions we promote:

+ +
// id greater than 5
+Post.where({ id_gt: 5 }).all()
+
+// id greater than or equal to 5
+Post.where({ id_gte: 5 }).all()
+
+// id less than 5
+Post.where({ id_lt: 5 }).all()
+
+// id less or equal to 5
+Post.where({ id_lte: 5 }).all()
+
+// title starts with "foo"
+Post.where({ title: { prefix: "foo" } }).all()
+
+// OR these two values
+Post.where({ status_or: ['draft', 'review'] })
+
+// AND these two values (default)
+Post.where({ status: ['draft', 'review'] })
+ +

Escaping Values

+ +

Graphiti treats a comma as a delimiter of multiple values. To escape the +comma and tell Graphiti this is a single value, wrap it in {{curlies}}:

+ +
Post.where({ title: "{{Hello World, here I am}}" })
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/reads/includes.html b/website/static/1.13/js/reads/includes.html new file mode 100644 index 00000000..3c2b9ee2 --- /dev/null +++ b/website/static/1.13/js/reads/includes.html @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Includes

+ +

Use #includes() to “sideload” associations:

+ +
Post.includes("comments").all()
+ +
+

/posts?include=comments

+
+ +

You can also pass an array of associations:

+ +
Post.includes(["blog", "comments"]).all()
+ +
+

/posts?include=blog,comments

+
+ +

Or an object for nested associations:

+ +
Post.includes(["blog", { comments: "author" }]).all()
+ +
+

/posts?include=blog,comments.author

+
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/reads/index.html b/website/static/1.13/js/reads/index.html new file mode 100644 index 00000000..ef665e2f --- /dev/null +++ b/website/static/1.13/js/reads/index.html @@ -0,0 +1,497 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Reads

+ +

The interface for read operations is a simpler version of the +ActiveRecord Query Interface. +Instead of generating SQL, we’ll be generating JSONAPI requests.

+ +

Basic Finders

+ +

Execute queries with .all(), find(), or .first():

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
let response = await Post.all()
+response.data // array of Post instances
+  
+ +
Post.all().then(function(response) {
+  response.data // array of Post instances
+});
+  
+ +
+
+

GET /posts

+
+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
let response = await Post.find(123)
+response.data // Post instance
+  
+ +
Post.find(123).then(function(response) {
+  response.data // Post instance
+});
+  
+ +
+
+

GET /posts/123

+
+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
let response = await Post.first()
+response.data // Post instance
+  
+ +
Post.first().then(function(response) {
+  response.data // Post instance
+});
+  
+ +
+
+

GET /posts?page[size]=1

+
+ +

Composable Queries with Scopes

+ +

The beauty of ORMs is their ability to compose queries. We’ll be doing +this by chaining together Scopes (query fragments). All of the methods +you see on this page can be chained together - the request will not fire +until the chain ends with all(), first(), or find. Example:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
let scope = Post.order({ name: "desc" })
+
+if (someCheckboxIsChecked) {
+  scope = scope.where({ important: true })
+} else {
+  scope = scope.where({ important: false })
+}
+
+scope.all() // request fires
+  
+ +
var scope = Post.order({ name: "desc" });
+
+if (someCheckboxIsChecked) {
+  scope = scope.where({ important: true });
+} else {
+  scope = scope.where({ important: false });
+}
+
+scope.all() // request fires
+  
+ +
+
+

/posts?sort=-name&filter[important]=true

+

/posts?sort=-name&filter[important]=false

+
+ +

In practice, you’ll probably have some scopes you want to re-use across +different contexts. A best practice is to store these scopes as class +methods (static methods) in the model:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
class Post extends ApplicationRecord {
+  // ... code ...
+  static superImportant() {
+    return this
+      .where({ ranking_gt: 8 })
+      .order({ ranking: 'desc' })
+      .stats({ total 'count' })
+  }
+}
+
+// get 10 super important posts
+let scope = Post.superImportant().per(10)
+scope.all() // fire query
+  
+ +
const Post = ApplicationRecord.extend({
+  // ... code ...
+  static: {
+    superImportant() {
+      return this
+        .where({ ranking_gt: 8 })
+        .order({ ranking: 'desc' })
+        .stats({ total 'count' })
+    }
+  }
+})
+
+// get 10 super important posts
+var scope = Post.superImportant().per(10);
+scope.all() // fire query
+  
+ +
+
+

/posts?sort=-ranking&stats[total]=count&page[size]=10&filter[ranking_gt]=8

+
+ +

Metadata

+ +

The meta information of the +JSONAPI response is available as a POJO on the response:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
let response = await Post.all()
+response.meta // { stats: { total: { count: 100 } } }
+  
+ +
await Post.all().then(function(response) {
+  response.meta // { stats: { total: { count: 100 } } }
+})
+  
+ +
+ +

Promises and Async/Await

+ +

The result of all(), first() or find is a Promise. The promise will resolve to a Response object.

+ +

A Response object has three keys - data, meta, and raw. data - the one +you’ll be using the most - will be a Model instance (or array of +Model) instances. meta will be the Meta Information returned by the API (mostly used for statistics in our case). raw is only used to introspect the raw response document.

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
Post.all().then((response) => {
+  response.data // array of Post instances
+  response.meta // js object from the server
+  response.raw // js response document
+})
+  
+ +
Post.all().then(function(response) {
+  response.data // array of Post instances
+  response.meta // js object from the server
+  response.raw // js response document
+});
+  
+ +
+
+

/posts

+
+ +

Hopefully you’re running in an environment that supports +ES7’s Async/Await. This makes things even easier:

+ +
let { data } = await Post.all()
+data // array of Post instances
+
+// alternatively
+
+let posts = (await Post.all()).data
+posts // array of Post instances
+ +
+

/posts

+
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/reads/nested-queries.html b/website/static/1.13/js/reads/nested-queries.html new file mode 100644 index 00000000..4345b392 --- /dev/null +++ b/website/static/1.13/js/reads/nested-queries.html @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Nested Queries

+ +

We can nest all read operations at any level of the graph. Let’s say we wanted to +fetch all Posts and their Comments…but only return comments that +are active, sorted by created_at descending. We can create a +Comment scope as normal, then #merge() it into our Post scope:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
let commentScope = Comment
+  .where({ active: true })
+  .order({ created_at: "desc" })
+Post
+  .includes("comments")
+  .merge({ comments: commentScope })
+  .all()
+ +
var commentScope = Comment
+  .where({ active: true })
+  .order({ created_at: "desc" })
+Post
+  .includes("comments")
+  .merge({ comments: commentScope })
+  .all()
+ +
+
+

/posts?include=comments&filter[comments][active]=true&sort=-comments.active

+
+ +

Because this can get verbose, it’s often desirable to store it on +the class:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
class Comment extends ApplicationRecord {
+  // ... code ...
+  static recent() {
+    return this
+      .where({ active: true })
+      .order({ created_at: "desc" })
+  }
+}
+
+Post.merge({ comments: Comment.recent() }).all()
+ +
const Comment = ApplicationRecord.extend({
+  // ... code ...
+  static: {
+    recent: function() {
+      return this
+        .where({ active: true })
+        .order({ created_at: "desc" })
+    }
+  }
+})
+
+Post
+  .includes("comments")
+  .merge({ comments: Comment.recent() })
+  .all()
+ +
+ +

Any number of scopes can be merged in. Just remember to #include() +and #merge() relationship names as the server understands them:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
class Dog extends ApplicationRecord {
+  @BelongsTo() person: Person
+}
+
+// We've modeled this as Dog > person in javascript
+// And Person is jsonapiType "people"
+// But the server defined the relationship as "owner"
+Dog.includes("owner").merge({ owner: Person.limitedFields() })
+ +
const Dog = ApplicationRecord.extend({
+  // ... code ...
+  methods: {
+    person: belongsTo()
+  }
+})
+
+// We've modeled this as Dog > person in javascript
+// And Person is jsonapiType "people"
+// But the server defined the relationship as "owner"
+Dog.includes("owner").merge({ owner: Person.limitedFields() })
+ +
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/reads/pagination.html b/website/static/1.13/js/reads/pagination.html new file mode 100644 index 00000000..c220855a --- /dev/null +++ b/website/static/1.13/js/reads/pagination.html @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Pagination

+ +

Use #per() to set the limit per page:

+ +
Post.per(10).all()
+ +
+

/posts?page[size]=10

+
+ +

Use #page() to set the current page:

+ +
Post.page(5).all()
+ +
+

/posts?page[number]=5

+
+ +

When chained together (10 per page, the 5th page):

+ +
Post.page(5).per(10).all()
+ +
+

/posts?page[size]=10&page[number]=5

+
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/reads/sorting.html b/website/static/1.13/js/reads/sorting.html new file mode 100644 index 00000000..6e941551 --- /dev/null +++ b/website/static/1.13/js/reads/sorting.html @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Sorting

+ +

Use #order() to sort.

+ +

If passed a string, it will default to ascending:

+ +
Post.order("title").all()
+ +
+

/posts?sort=title

+
+ +

Otherwise, pass an object:

+ +
Post.order({ title: "desc" }).all()
+ +
+

/posts?sort=-title

+
+ +

For multisort, simply chain multiple #order() clauses:

+ +
Post
+  .order({ title: "desc" })
+  .order("ranking")
+  .all()
+ +
+

/posts?sort=-title,ranking

+
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/reads/statistics.html b/website/static/1.13/js/reads/statistics.html new file mode 100644 index 00000000..bc74c883 --- /dev/null +++ b/website/static/1.13/js/reads/statistics.html @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Statistics

+ +

Use #stats() to request statistics. Access stats within meta:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
let { data } = await Post.stats({ total: "count" }).all()
+data.meta.stats.total.count // the total count
+ +
Post.stats({ total: "count" }).all().then(function(response) {
+  response.meta.stats.total.count // the total count
+})
+ +
+
+

/posts?stats[total]=count

+
+ +

Stats are always independent of pagination. If you request the total count, you’ll get the total count even if you’re limiting to 10 per page. This means to get only statistics - avoid returning Post instances altogether - simply request 0 results per page:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
let { data } = await Post.per(0)stats({ total: "count" }).all()
+data.meta.stats.total.count // the total count
+ +
Post
+  .per(0)
+  .stats({ total: "count" })
+  .all().then(function(response) {
+    response.meta.stats.total.count // the total count
+  })
+ +
+
+

/posts?stats[total]=count&page[size]=0

+
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/state-syncing.html b/website/static/1.13/js/state-syncing.html new file mode 100644 index 00000000..994b9531 --- /dev/null +++ b/website/static/1.13/js/state-syncing.html @@ -0,0 +1,340 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

State Syncing

+ +

You may have encountered state management libraries like Flux, +Redux or Vuex. These are fantastic libraries, but their usefulness is lessened with Spraypaint. As a full-fledged model layer, Spraypaint manages state for you, automatically.

+ +

If you opt-in to this feature:

+ +
ApplicationRecord.sync = true
+ +

Instances will sync up whenever the server tells us about updated state. +Consider the scenario where an instance is initially loaded, then separately polled in the background:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let person = (await Person.find(1)).data
+
+  let poll = () => {
+    await Person.find(1)
+    setTimeout(poll, 1000)
+  }
+  poll()
+  
+ +
  Person.find(1).then(function(response) {
+    var person = response.data;
+  });
+
+  var poll = function() {
+    Person.find(1);
+    setTimeout(poll, 1000);
+  }
+  poll()
+  
+ +
+ +

Note that our poll() function never assigns or updates person. +But if the server returns an updated name attribute, person.name +will be automatically updated. This is true even if person.name +is bound in 17 different nested components.

+ +

Instances can still update their attributes independently - we only sync +when the server returns updated data:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let instanceA = (await Person.find(1)).data
+  let instanceB = (await Person.find(1)).data
+
+  instanceA.name // "Jane"
+  instanceB.name // "Jane"
+
+  instanceB.name = "Silvia"
+  instanceA.name // "Jane"
+  instanceB.name // "Silvia"
+
+  await instanceB.save()
+  instanceA.name // "Silvia"
+  instanceB.name // "Silvia"
+  
+ +
  var instanceA, instanceB;
+  Person.find(1).then(function(response) {
+    instanceA = response.data;
+  });
+  Person.find(1).then(function(response) {
+    instanceB = response.data;
+  });
+
+  instanceA.name // "Jane"
+  instanceB.name // "Jane"
+
+  instanceB.name = "Silvia"
+  instanceA.name // "Jane"
+  instanceB.name // "Silvia"
+
+  instanceB.save().then(function() {
+    instanceA.name // "Silvia"
+    instanceB.name // "Silvia"
+  });
+  
+ +
+ +

Gotchas

+ +

Under the hood, instances are listening for updates from a central data +store. This means that you’ll want to remove listeners whenever you no +longer need the instance - otherwise it will never be garbage collected +properly. To remove a listener:

+ +
instance.unlisten()
+ +

In practice, when developing in a SPA, you’ll want to #unlisten() +whenever a view is destroyed and model instances no longer need to be referenced. If +you are using VueJS, this is done automatically by adding spraypaint-vue +to your application.

+
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/writes/deferred.html b/website/static/1.13/js/writes/deferred.html new file mode 100644 index 00000000..56a8def9 --- /dev/null +++ b/website/static/1.13/js/writes/deferred.html @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Deferred Action

+ +

If your update or destroy action takes a long time then the server can respond with status code 202 Accepted and include background job object in the payload.

+ +

Example response:

+
+ +
HTTP/1.1 202 Accepted
+Content-Type: application/vnd.api+json
+
+{
+  "data": {
+    "type": "background_jobs",
+    "id": "550e8400-e29b-41d4-a716-446655440000",
+    "attributes": {
+      "status": "pending"
+    }
+  }
+}
+  
+ +
+ +

You will need to give the model object a callback called onDeferredDestroy or onDeferredUpdate. Spraypaint will then call your callback with the deserialized object included in the payload.

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
let person = new Person({ firstName: 'Jane' })
+person.onDeferredUpdate = (job: any) => {
+  handleBackgroundJob(job);
+}
+person.save()
+
+person.onDeferredDestroy = (job: any) => {
+  handleBackgroundJob(job);
+}
+person.destroy()
+  
+ +
const person = new Person({ firstName: 'Jane' });
+person.onDeferredUpdate = (job) => {
+  handleBackgroundJob(job);
+};
+person.save();
+
+person.onDeferredDestroy = (job) => {
+  handleBackgroundJob(job);
+};
+person.destroy();
+  
+ +
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/writes/dirty-tracking.html b/website/static/1.13/js/writes/dirty-tracking.html new file mode 100644 index 00000000..0ae7b108 --- /dev/null +++ b/website/static/1.13/js/writes/dirty-tracking.html @@ -0,0 +1,399 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Dirty Tracking

+ +

When an attribute has been modified, but has not yet been saved to the +server, it is considered “dirty”. Use #isDirty() to see if any attribute is dirty, use the #changes() method to see all dirty attributes.

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let post = await Post.first()
+  post.title // "original"
+  post.isDirty() // false
+  post.changes() // {}
+
+  post.title = "changed"
+  post.isDirty() // true
+  post.changes() // { title: ["original", "changed"] }
+
+  await post.save()
+  post.isDirty() // false
+  post.changes() // {}
+  
+ +
  Post.first().then(function(response) {
+    var post = response.data;
+
+    post.title; // "original"
+    post.isDirty(); // false
+    post.changes(); // {}
+
+    post.title = "changed";
+    post.isDirty(); // true
+    post.changes(); // { title: ["original", "changed"] }
+
+    post.save().then(function(success) { // true
+      post.isDirty(); // false
+      post.changes(); // {}
+    });
+  });
+  
+ +
+ +
+

Remember, only dirty attributes are sent to the server when #save() +is called.

+
+ +

#isDirty() can take into account relationships - just pass a +string, array, or object or relationship names. A relationship is +considered dirty if:

+ +
    +
  • Any objects in the relationship have dirty attributes
  • +
  • An object was removed from a hasMany relationship
  • +
  • An object was added to a hasMany relationship
  • +
  • Any object within the relationship was replaced with a different +object.
  • +
+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let post = await Post.first()
+  post.comments[0].text = "my comment"
+  post.isDirty("comments") // true
+
+  post = await Post.first()
+  post.comments.push(new Comment())
+  post.isDirty("comments") // true
+
+  post = await Post.first()
+  post.comments.splice(1, 1)
+  post.isDirty("comments") // true
+
+  post = await Post.first()
+  post.blog // an existing Blog instance
+  post.blog = (await Blog.first()).data
+  post.isDirty("blog") // true
+
+  // check nested relationships
+  post.isDirty(["blog", { comments: "author" }])
+  
+ +
  Post.first().then(function(response) {
+    var post = response.data;
+    post.comments[0].text = "my comment";
+    post.isDirty("comments"); // true
+  });
+
+  Post.first().then(function(response) {
+    var post = response.data;
+    post.comments.push(new Comment());
+    post.isDirty("comments"); // true
+  });
+
+  Post.first().then(function(response) {
+    var post = response.data;
+    post.comments.splice(1, 1);
+    post.isDirty("comments"); // true
+  });
+
+  Post.first().then(function(response) {
+    var post = response.data;
+    post.blog; // an existing Blog instance
+
+    Blog.first().then(function(blog) {
+      post.blog = (await Blog.first()).data
+      post.isDirty("blog") // true
+    });
+  });
+
+  // check nested relationships
+  post.isDirty(["blog", { comments: "author" }])
+  
+ +
+ +

If you need to reset dirty tracking, call #reset()

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let post = await Post.first()
+  post.title // "original"
+  post.title = "changed"
+  post.isDirty() // true
+  post.reset()
+  post.title // "changed"
+  post.isDirty() // false
+ +
  Post.first().then(function(post) {
+    post.title; // "original"
+    post.title = "changed";
+    post.isDirty() // true
+    post.reset();
+    post.title; // "original"
+    post.isDirty() // false
+  });
+ +
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/writes/index.html b/website/static/1.13/js/writes/index.html new file mode 100644 index 00000000..a41396af --- /dev/null +++ b/website/static/1.13/js/writes/index.html @@ -0,0 +1,391 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Writes

+ +

Similar to ActiveRecord, you can simply call #save() on a model +instance. Spraypaint will create (POST) or update (PATCH) as needed.

+ +

#save() returns a Promise that will resolve a boolean - true +when the server returns a 200-ish response code, false when the server +returns a 422 response code (see +validations). As always, anything else will +reject the promise.

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let blog = new Blog({ title: "My Blog" })
+  let success = await blog.save() // POST /blogs
+  console.log(success) // true/false
+
+  blog.title = "Updated Title"
+  success = await blog.save() // PUT /blogs/:id
+  console.log(success) // true/false
+  
+ +
  var blog = new Blog({ title: "My Blog" });
+  // POST /blogs
+  blog.save().then(function(success) {
+    console.log(success); // true/false
+
+    blog.title = "Updated Title":
+    // PUT /blogs/:id
+    blog.save().then(function(success) {
+      console.log(success) // true/false
+    });
+  });
+  
+ +
+ +

After saving, the instance will automatically pick up any +server-assigned attributes:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let post = new Post()
+  await post.save()
+  post.id // server-assigned value
+  post.createdAt // server-assigned value
+  
+ +
  var post = new Post();
+  post.save().then(function(success) {
+    post.id // server-assigned value
+    post.createdAt // server-assigned value
+  });
+  
+ +
+ +

If a Model was instantiated with data from the server, isPersisted +will return true. This means that we can assign IDs on the client +without any adverse behavior; we can also manually mark objects as +persisted for testing purposes:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let blog = new Blog({ id: 123 })
+  blog.isPersisted // false
+  await blog.save() // POST /blogs
+  blog.isPersisted // true
+  blog.id // 123
+
+  // Manually mark an instance as persisted
+  blog = new Blog({ id: 123 })
+  blog.isPersisted = true
+  await blog.save() // PUT /blogs/123
+  
+ +
  var blog = new Blog({ id: 123 });
+  blog.isPersisted // false
+  // POST /blogs
+  blog.save().then(function(response) {
+    blog.isPersisted // true
+    blog.id // 123
+  });
+
+  // Manually mark an instance as persisted
+  var blog = new Blog({ id: 123 });
+  blog.isPersisted = true
+  blog.save() // PUT /blogs/123
+  
+ +
+ +

Notably, only dirty (changed) attributes will be sent to the server. This prevents race conditions and unexpected side-effects. In the following example, Post has attributes title, description, and createdAt:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let post = (await Post.first())
+  post.title = "updated"
+  // ONLY title sent to the server
+  await post.save()
+  // Title is now synced with the server
+  post.description = "updated"
+  // ONLY description sent to the server
+  await post.save()
+  
+ +
  Post.first().then(function(response) {
+    var post = response.data;
+    post.title = "updated";
+    // ONLY title sent to the server
+    post.save().then(function(response) {
+      // Title is now synced with the server
+      post.description = "updated";
+      // ONLY description sent to the server
+      post.save();
+    });
+  });
+  
+ +
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/writes/nested.html b/website/static/1.13/js/writes/nested.html new file mode 100644 index 00000000..44c3cd25 --- /dev/null +++ b/website/static/1.13/js/writes/nested.html @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Nested Writes

+ +

You can write a Model and all of its relationships in a single +request. Keep in mind normal dirty tracking rules still apply - nothing +is sent to the server unless it is dirty.

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let author = new Author()
+  let comment = new Comment({ author })
+  let post = new Post({ comments: [comment] })
+
+  // post.save({ with: "comments" })
+  // post.save({ with: ["comments", "blog"] })
+  post.save({ with: { comments: 'author' }})
+  
+ +
  var author = new Author();
+  var comment = new Comment({ author: author });
+  var post = new Post({ comments: [comment] });
+
+  // post.save({ with: "comments" })
+  // post.save({ with: ["comments", "blog"] })
+  post.save({ with: { comments: "author" }});
+  
+ +
+ +

Use model.isMarkedForDestruction = true to delete the associated +object. Use model.isMarkedForDisassociation = true to remove the association +without deleting the underlying object:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let post = (await Post.includes("comments").first()).data
+  post.comments[0].isMarkedForDestruction = true
+  post.comments[1].isMarkedForDisassociation = true
+
+  // destroys the first comment
+  // disassociates the second comment
+  await post.save({ with: "comments" })
+  
+ +
  Post.includes("comments").first().then(function(response) {
+    var post = response.data;
+    post.comments[0].isMarkedForDestruction = true;
+    post.comments[1].isMarkedForDisassociation = true;
+
+    // destroys the first comment
+    // disassociates the second comment
+    post.save({ with: "comments" })
+  });
+  
+ +
+ +

You may want to send only the id of the related object to the server - ensuring the models are associated without updating attributes by +accident. Just add .id to the relationship name:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  post.save({ with: "comments.id" })
+  
+ +
  post.save({ with: "comments.id" })
+  
+ +
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/js/writes/validations.html b/website/static/1.13/js/writes/validations.html new file mode 100644 index 00000000..47d33632 --- /dev/null +++ b/website/static/1.13/js/writes/validations.html @@ -0,0 +1,272 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Spraypaint + the isomorphic, framework-agnostic Graphiti ORM +

+ + + +
+

Validations

+ +

JSONAPI Suite is already set up to return validation errors with a +422 response code and JSONAPI-compliant errors payload. Those errors will be automatically assigned, and removed on subsequent requests:

+ +
+
+ Typescript +
+
+ Javascript +
+
+ +
+ +
  let success = await post.save()
+  console.log(success) // false
+  post.errors.title // { message: "Can't be blank", ... }
+  post.title = "no longer blank"
+  success = await post.save()
+  console.log(success) // true
+  post.errors // {}
+  
+ +
  post.save().then(function(success) {
+    console.log(success) // false
+    post.errors.title // { message: "Can't be blank", ... }
+    post.title = "no longer blank"
+    post.save().then(function(success) {
+      console.log(success); // true
+      post.errors // {}
+    });
+  })
+  
+ +
+ + +
+ +
+
+
+ + + + + + diff --git a/website/static/1.13/quickstart.html b/website/static/1.13/quickstart.html new file mode 100644 index 00000000..b587025b --- /dev/null +++ b/website/static/1.13/quickstart.html @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

Quickstart

+ + + +
+ Zero to API in 5 minutes +
+
+ +

This quickstart will use Rails with ActiveRecord to give an overview of +Graphiti functionality on familiar ground. For a more in-depth breakdown, head to the +Guides.

+ +

If the below seems too “magical”, don’t worry - we’re just applying some +sensible defaults to get started quickly.

+ + + +

+ Installation +

+
+ +

Let’s start with a classic Rails blog. We’ll use a template to handle some of the boilerplate. Just run this command and accept all the defaults for now:

+ +
$ rails new blog --api -m /1.13/template
+ +

Feel free to run git diff if you’re interested in the +particulars; this is mostly just installing gems and including modules.

+ +
+

Note: if a network issue prevents you from pointing to this URL +directly, you can download the file and and run this command as -m +/path/to/template

+
+ +

Alternatively, you can add to an existing project.

+ + + +

+ Defining a Resource +

+
+ +

A Resource defines how to query and persist your Model. In other +words: a Model is to the database as Resource is to the API. So +first, let’s define our Model:

+ +
$ bundle exec rails generate model Post title:string upvotes:integer active:boolean
+$ bundle exec rails db:migrate
+ +

Now we can use the built-in generator to define our Resource, +corresponding Endpoint, and +Integration Tests.

+ +
$ bundle exec rails g graphiti:resource Post title:string upvotes:integer active:boolean
+ +

You’ll see a number of files created. Now run your app!:

+ +
$ bundle exec rails s
+ +

Verify http://localhost:3000/api/v1/posts renders JSON correctly. +Now we just need data.

+ + + +
+ Seeding Data +
+
+ +

Edit db/seeds.rb to create a few Posts:

+ +
Post.create!(title: 'My title', upvotes: 10, active: true)
+Post.create!(title: 'Another title', upvotes: 20, active: false)
+Post.create!(title: 'OMG! A title', upvotes: 30, active: true)
+ +

And run the script:

+ +
$ bundle exec rails db:seed
+ +

Now load http://localhost:3000/api/v1/posts. You should have 3 Posts in +your database!

+ +
+ + + +

+ Querying +

+
+ +

Now that we’ve defined our Resource and seeded some data, let’s see +what query functionality we have. We’ve listed all Posts at +http://localhost:3000/api/v1/posts. Let’s see what we can do:

+ +
    +
  • Sort +
      +
    • By title, ascending: +
        +
      • URL: /api/v1/posts?sort=title
      • +
      • SQL: SELECT * FROM posts ORDER BY title ASC
      • +
      +
    • +
    • By title, descending: +
        +
      • URL: /api/v1/posts?sort=-title
      • +
      • SQL: SELECT * FROM posts ORDER BY title DESC
      • +
      +
    • +
    +
  • +
  • Paginate: +
      +
    • 2 Per page: +
        +
      • URL: /api/v1/posts?page[size]=2
      • +
      • SQL: SELECT * FROM posts LIMIT 2
      • +
      +
    • +
    • 2 Per page, second page: +
        +
      • URL: /api/v1/posts?page[size]=2&page[number]=2
      • +
      • SQL: SELECT * FROM posts LIMIT 2 OFFSET 2
      • +
      +
    • +
    +
  • +
  • Sparse Fieldsets: +
      +
    • Only render title, not active: +
        +
      • URL: /api/v1/posts?fields[posts]=title
      • +
      • SQL: SELECT * from posts (optimizing this query is on the roadmap)
      • +
      +
    • +
    +
  • +
  • Filter: +
      +
    • Simple: +
        +
      • URL: /api/v1/posts?filter[title]=my title
      • +
      • SQL: SELECT * FROM posts WHERE title = "My title!"
      • +
      +
    • +
    • Case Insensitive: +
        +
      • URL: /api/v1/posts?filter[title][eql]=My title
      • +
      • SQL: SELECT * FROM posts WHERE lower(title) = "my title!"
      • +
      +
    • +
    • Prefix: +
        +
      • URL: /api/v1/posts?filter[title][prefix]=my
      • +
      • SQL: SELECT * FROM posts WHERE lower(title) LIKE 'my%'
      • +
      +
    • +
    • Suffix: +
        +
      • URL: /api/v1/posts?filter[title][suffix]=title
      • +
      • SQL: SELECT * FROM posts WHERE lower(title) LIKE '%title!'
      • +
      +
    • +
    • Contains: +
        +
      • URL: /api/v1/posts?filter[title][match]=itl
      • +
      • SQL: SELECT * FROM posts WHERE lower(title) LIKE '%itl%'
      • +
      +
    • +
    • Greater Than: +
        +
      • URL: /api/v1/posts?filter[upvotes][gt]=20
      • +
      • SQL: SELECT * FROM posts WHERE upvotes > 20
      • +
      +
    • +
    • Greater Than or Equal To: +
        +
      • URL: /api/v1/posts?filter[upvotes][gte]=20
      • +
      • SQL: SELECT * FROM posts WHERE upvotes >= 20
      • +
      +
    • +
    • Less Than: +
        +
      • URL: /api/v1/posts?filter[upvotes][lt]=20
      • +
      • SQL: SELECT * FROM posts WHERE upvotes < 20
      • +
      +
    • +
    • Less Than or Equal To: +
        +
      • URL: /api/v1/posts?filter[upvotes][lte]=20
      • +
      • SQL: SELECT * FROM posts WHERE upvotes <= 20
      • +
      +
    • +
    • Any filter not whitelisted will raise JsonapiCompliable::BadFilter +error.
    • +
    • All filter logic can be customized
    • +
    • Customizations can be DRYed up and packaged into Adapters.
    • +
    +
  • +
  • Extra Fields: +
      +
    • Sometimes you want to request additional fields not part of a normal +response (perhaps they are computationally expensive).
    • +
    • This can be done like so:
    • +
    +
  • +
+ +
# app/resources/post_resource.rb
+extra_attribute :description, :string do
+  @object.active? ? 'Active Post' : 'Inactive Post'
+end
+ +
    +
  • URL: /api/v1/posts?extra_fields[posts]=description
  • +
  • SQL: SELECT * FROM posts
  • +
  • +

    You can conditionally eager load data or further customize this + logic.

    +
  • +
  • Statistics: +
      +
    • Useful for search grids - “Find me the first 10 active posts, and +the total count of all posts”.
    • +
    • URL: /api/v1/posts?stats[total]=count
    • +
    • SQL: SELECT count(*) from posts
    • +
    • Combine with filters and the count will adjust accordingly.
    • +
    • There are a number of built-in stats, you can also add your own.
    • +
    • +

      This is rendered in the meta section of the response:

      + +

      meta_total_count

      +
    • +
    • View Documentation
    • +
    +
  • +
  • Error Handling: +
      +
    • Your app will always render a JSONAPI-compliable error response.
    • +
    • Cause an error:
    • +
    +
  • +
+ +
# app/controllers/posts_controller.rb
+def index
+  raise 'foo'
+end
+ +
    +
  • +

    The default payload you’d see in production:

    + +

    error_payload

    +
  • +
  • +

    Different errors can be customized with different response codes, + JSON, and side-effects. See more in the Error Handling + Guide.

    +
  • +
+ + + +

+ Persisting +

+
+ +

Resources can Create, Update, and Delete (and you can persist multiple +Resources in a single request). The best way to observe this behavior is +to take a look at the tests the generator created. One example:

+ +
# spec/api/v1/employees/create_spec.rb
+subject(:make_request) do
+  jsonapi_post "/api/v1/employees", payload
+end
+
+describe 'basic create' do
+  let(:payload) do
+    {
+      data: {
+        type: 'employees',
+        attributes: {
+          first_name: 'Jane'
+        }
+      }
+    }
+  end
+
+  it 'works' do
+    expect(EmployeeResource).to receive(:build).and_call_original
+    expect {
+      make_request
+    }.to change { Employee.count }.by(1)
+    expect(response.status).to eq(201)
+  end
+end
+ +

Read more about Persistence and +Testing Persistence.

+ + + +

+ Adding Relationships +

+
+ +

Let’s start by defining our Model:

+ +
$ bundle exec rails g model Comment post_id:integer body:text active:boolean
+$ bundle exec rails db:migrate
+ +
# app/models/post.rb
+has_many :comments
+
+# app/models/comment.rb
+belongs_to :post
+ +

…and corresponding Resource object:

+ +
$ bundle exec rails g graphiti:resource Comment body:string active:boolean created_at:datetime
+ +

Configure the relationship in PostResource:

+ +
# app/resources/post_resource.rb
+has_many :comments
+ +

And allow filtering Comments based on the Post id:

+ +
# app/resources/comment_resource.rb
+attribute :post_id, :integer, only: [:filterable]
+ +

This code:

+ +
    +
  • Allows eager-loading the relationship. +
      +
    • URL: /api/v1/posts?include=comments
    • +
    • SQL: SELECT * FROM comments WHERE post_id = 123
    • +
    +
  • +
  • Generates a Link for +lazy-loading.
  • +
  • Will use CommentResource for querying logic (so we can Deep +Query, e.g. +“only return the latest 3 active comments”).
  • +
  • By default, this will generate the query CommentResource.all(filter: +{ post_id: 123 }), but relationships can be customized
  • +
+ +

You should now be able to hit /api/v1/comments with all the same +functionality as before. We just need to seed data.

+ + + +

+ Seeding Relationships +

+
+ +

Start by clearing out your database:

+ +
$ bundle exec rails db:migrate:reset
+ +

Replace your db/seeds.rb with this code to persist one Post and +three Comments:

+ +
comment1 = Comment.new(body: 'comment one', active: true)
+comment2 = Comment.new(body: 'comment two', active: false)
+comment3 = Comment.new(body: 'comment three', active: true)
+
+Post.create! \
+  title: 'My title!',
+  active: true,
+  comments: [comment1, comment2, comment3]
+ +

And run it:

+ +
$ bundle exec rails db:seed
+ + + +

+ Relationship Usage +

+
+ +

Now let’s fetch a Post and filtered Comments in a single request:

+ +

/api/v1/posts?include=comments

+ +

Any logic in CommentResource is available to us. Let’s sort the +comments by created_at descending:

+ +

/api/v1/posts?include=comments&sort=-comments.created_at.

+ +

Logic from CommentResource is accessible at the /api/v1/comments +endpoint, and reusable when eager-loading Comments at /api/v1/posts:

+ +
    +
  • /api/v1/comments?filter[active]=true
  • +
  • /api/v1/posts?include=comments&filter[comments.active]=true
  • +
+ +

This is why Resource objects exist: they provide an interface to +reuse code across multiple Endpoints.

+ +

Also note: just as we can query a graph of Resources in a single +request, we can persist a graph of Resources in a single request. See +Sideposting.

+ + + +

+ Exploring with Vandal +

+
+ +

+ +

+ +

Vandal is the Graphiti UI. It introspects your schema to make data +exploration a breeze. The above screenshot shows Vandal fetching posts +and comments from our blog.

+ +

To run Vandal, hit http://localhost:3000/api/v1/vandal. Click a +relationship once to include it in the response, click a second time to +edit deep query logic for the associated Resource.

+ +

If you’ve included an association, you can click a table row to view associated data. +The below screenshot is hitting /posts, and including only comments +with the text “two”:

+ +

+ +

+ +

Click here to play with Vandal hitting our Employee Directory Tutorial. Initial load might take longer as it runs on a free Heroku instance.

+ + + +

+ What's next +

+
+ +

We have a full CRUD API with robust querying functionality, and the +ability to combine relationships for both reads and writes. But what +happens when you need to customize the sorting logic? What about replacing +ActiveRecord with an alternate persistence layer, or avoiding Rails +altogether?

+ +

These are important topics that Graphiti was built to address. To +learn more about advanced usage and customization, we suggest following +the Tutorial and reading through the +Guides.

+ +

For help with specific use cases, join our Discord chat!

+ + + +

+ Testing +

+
+ +

This Quickstart is meant to get you up-and-running quickly, so we didn’t +write tests. But in Graphiti testing is the easiest, most pleasant way +to develop your application.

+ +

Even if you’re not normally a TDDer, we highly recommend reading through +our Integration Testing Guide.

+ +


+

+ +
+
+
+ + + + + + diff --git a/website/static/1.13/template b/website/static/1.13/template new file mode 100644 index 00000000..8c092660 --- /dev/null +++ b/website/static/1.13/template @@ -0,0 +1,161 @@ + +Thor::Base.shell = Thor::Shell::Color +require 'yaml' + +def truthy?(statement) + val = ask(statement) + ['y', 'yes', ''].include?(val) +end + +def eval_template(name) + instance_eval(File.read(File.dirname(__FILE__) + "/#{name}.rb")) +end + +def update_config!(attrs) + config = File.exists?('.graphiticfg.yml') ? YAML.load_file('.graphiticfg.yml') : {} + config.merge!(attrs) + File.open('.graphiticfg.yml', 'w') { |f| f.write(config.to_yaml) } +end + +def api_namespace + @api_namespace ||= begin + ns = prompt \ + header: "What is your API namespace?", + description: "This will be used as a route prefix, e.g. if you want the route '/books_api/v1/authors' your namespace would be '/books_api/v1'", + default: '/api/v1' + update_config!('namespace' => ns) + ns + end +end + +def prompt(header: nil, description: nil, default: nil) + say(set_color("\n#{header}", :magenta, :bold)) if header + say("\n#{description}") if description + answer = ask(set_color("\n(default: #{default}):", :magenta, :bold)) + answer = default if answer.blank? && default != 'nil' + say(set_color("\nGot it!\n", :white, :bold)) + answer +end + +welcome = <<-STR +\n +Welcome to the Graphiti generator! +======================================= + +This will take care of some boilerplate for you, like adding gem dependencies and rspec helpers. + +If you're worried there might be too much magic here, feel free to run 'git diff' at the end to see what happened. You can also learn more at our documentation website, https://jsonapi-suite.github.io/jsonapi_suite +STR + +say(set_color(welcome.rstrip, :cyan, :bold)) +api_namespace + +require 'rails/version' + +gem 'graphiti' +gem 'graphiti-rails' +gem 'vandal_ui' +gem 'kaminari', '~> 1.1' + +if Rails::VERSION::MAJOR == 5 + gem 'responders', '~> 2.4' +else + gem 'responders', '~> 3.0' +end + +gem_group :development, :test do + if Rails::VERSION::MAJOR == 5 + gem 'rspec-rails', '~> 3.5.2' + else + gem 'rspec-rails', '~> 4.0.0beta2' + end + + gem 'factory_bot_rails', '~> 5.0' + gem 'faker', '~> 2.5' # keep here for seeds.rb + gem 'graphiti_spec_helpers' +end + +gem_group :test do + gem 'database_cleaner', '~> 1.7' +end + +after_bundle do +unless Rails::VERSION::MAJOR > 6 +run 'bin/spring stop' +end + +git :init +git add: '.' +run "bundle binstub rspec-core" +rails_command "generate rspec:install" +run "rm -rf test" + +insert_into_file "spec/rails_helper.rb", :after => "require 'rspec/rails'\n" do + "require 'graphiti_spec_helpers/rspec'\n" +end + +insert_into_file "spec/rails_helper.rb", :after => "RSpec.configure do |config|\n" do + <<-STR + + # bootstrap database cleaner + config.before(:suite) do + DatabaseCleaner.strategy = :transaction + DatabaseCleaner.clean_with(:truncation) + end + + config.around(:each) do |example| + begin + DatabaseCleaner.cleaning do + example.run + end + ensure + DatabaseCleaner.clean + end + end + + STR +end + +insert_into_file "spec/rails_helper.rb", :after => "RSpec.configure do |config|\n" do + <<-STR + + config.before :each do + handle_request_exceptions(false) + end + STR +end + +insert_into_file "spec/rails_helper.rb", :after => "RSpec.configure do |config|\n" do + " config.include Graphiti::Rails::TestHelpers\n" +end + +insert_into_file "spec/rails_helper.rb", :after => "RSpec.configure do |config|\n" do + " config.include GraphitiSpecHelpers::Sugar\n" +end + +insert_into_file "spec/rails_helper.rb", :after => "RSpec.configure do |config|\n" do + " config.include GraphitiSpecHelpers::RSpec\n" +end + +insert_into_file "spec/rails_helper.rb", :after => "RSpec.configure do |config|\n" do + " config.include FactoryBot::Syntax::Methods\n" +end + +gsub_file "spec/rails_helper.rb", 'config.fixture_path = "#{::Rails.root}/spec/fixtures"' do |match| + "# #{match}" +end + +gsub_file "spec/rails_helper.rb", 'config.use_transactional_fixtures = true' do |match| + "# #{match}" +end + +run "mkdir spec/factories" + +rails_command('generate graphiti:install') +run 'bundle binstubs bundler --force' +rake('vandal:install') +say(set_color("\nYou're all set! +", :green, :bold)) +end + + diff --git a/website/static/1.13/tutorial/index.html b/website/static/1.13/tutorial/index.html new file mode 100644 index 00000000..adfff827 --- /dev/null +++ b/website/static/1.13/tutorial/index.html @@ -0,0 +1,250 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ +

+ +

Tutorial

+ +

This tutorial serves as a deeper-dive into Graphiti development, +building an Employee Directory application. We purposefully built this +to illustrate common - but non-trivial - scenarios present in many +applications.

+ +

A core concept of Graphiti is Test-First - the most pleasant way to +develop Graphiti is by starting with an integration test. But that can add a lot of noise to a tutorial like this. Though we’ll occasionally touch on testing - and the git diffs at the top of each section contain the necessary tests - we won’t test first for the purposes of this tutorial.

+ + + +


+
+
+
+

+ +
+
+
+ + + + + + diff --git a/website/static/1.13/tutorial/step_0.html b/website/static/1.13/tutorial/step_0.html new file mode 100644 index 00000000..de412bd4 --- /dev/null +++ b/website/static/1.13/tutorial/step_0.html @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +

Step 0: Bootstrapping

+ +
+

View the Code

+
+ +

Let’s start by creating a new Rails project. For help with an existing +project, check out Installation: From +Scratch.

+ +

We’ll use the -m option to install from a template, which will add a few gems and apply some setup boilerplate. Accept all the default options.

+ +
$ rails new employee_directory --api -m /1.13/template
+$ cd employee_directory
+ +
+

Note: if a network issue prevents you from pointing to this URL directly, you can download the file and and run this command as -m /path/to/template

+
+ +

Feel free to run git diff to see what the generator did, otherwise commit the result. You can now head to Step 1: Basic Resource, or continue reading to better understand the code.

+ +

Digging Deeper 🧐

+ +

You’ll see some boilerplate in config/routes.rb:

+ +
scope path: ApplicationResource.endpoint_namespace, defaults: { format: :jsonapi } do
+  # your routes go here
+end
+ +

This tells Rails that our API routes will be be prefixed - /api/v1 by default. It also +says that if no extension is in the URL (.json, .xml, etc), default +to the JSONAPI Specification.

+ +

Let’s look at the above ApplicationResource:

+ +
class ApplicationResource < Graphiti::Resource
+  self.abstract_class = true
+
+  # We'll be using ActiveRecord
+  self.adapter = Graphiti::Adapters::ActiveRecord
+
+  # Links are generated from base_url + endpoint_namespace
+  self.base_url = Rails.application.routes
+    .default_url_options[:host]
+  self.endpoint_namespace = '/api/v1'
+end
+ +

This should be pretty self-explanatory except for

+ +
self.base_url = Rails.application.routes
+  .default_url_options[:host]
+ +

This is configured in config/application.rb:

+ +
module EmployeeDirectory
+  class Application < Rails::Application
+    routes.default_url_options[:host] = ENV.fetch('HOST', 'http://localhost:3000')
+    # ... code ...
+  end
+end
+ +

When deriving and validating Links, we’ll use the HOST variable if +present, falling back to the Rails development default of +http://localhost:3000. This means our Links will look like:

+ +
"#{ENV['HOST']}/#{ApplicationRecord.endpoint_namespace}/#{Resource.type}"
+ +

For example:

+ +
http://my-website.com/api/v1/employees
+ +

Read more in the Links Guide.

+ +

Finally, there’s some boilerplate in ApplicationController:

+ +
class ApplicationController < ActionController::API
+  # Support JSON, XML, JSON:API
+  include Graphiti::Rails::Responders
+end
+ +

This gets respond_with working, via the Responders gem. To render simple nested JSON like default Rails, we’ll only need to add .json to the URL.

+ +

That’s it for basic setup!

+ +
+ + + +
+
+
+ + + + + + diff --git a/website/static/1.13/tutorial/step_1.html b/website/static/1.13/tutorial/step_1.html new file mode 100644 index 00000000..30d0994e --- /dev/null +++ b/website/static/1.13/tutorial/step_1.html @@ -0,0 +1,517 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +

Step 1: Basic Resource

+ +
+

View the Diff

+
+ +

We’ll be working with a single database table, employees:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
idfirst_namelast_nameagecreated_atupdated_at
1HomerSimpson392018-09-042018-09-04
2WaylonSmithers652018-09-042018-09-04
3MontyBurns1232018-09-042018-09-04
+ +

The Rails Stuff 🚂

+ +

Use the built-in generator to create the database table +and corresponding ActiveRecord model:

+ +
$ bin/rails g model Employee first_name:string last_name:string age:integer
+$ bin/rails db:migrate
+ +

Now let’s seed some random development data, using Faker (which was installed in Step 0):

+ +
# db/seeds.rb
+Employee.delete_all # Ensure the DB is cleaned each run
+
+100.times do
+  Employee.create! first_name: Faker::Name.first_name,
+    last_name: Faker::Name.last_name,
+    age: rand(20..80)
+end
+ +

Run this seed file with

+ +
$ bin/rails db:seed
+ +

The Graphiti Stuff 🎨

+ +

Just like Rails, Graphiti has built-in generators. Let’s generate +the corresponding Resource for our Employee model:

+ +
$ bin/rails g graphiti:resource Employee first_name:string last_name:string age:integer created_at:datetime updated_at:datetime
+ +

This generated a few things, but for now let’s focus on +EmployeeResource:

+ +
class EmployeeResource < ApplicationResource
+  attribute :first_name, :string
+  attribute :last_name, :string
+  attribute :age, :integer
+  attribute :created_at, :datetime, writable: false
+  attribute :updated_at, :datetime, writable: false
+end
+ +

This code defined the RESTful Resource we want our API to expose. Let’s run our server and see what it does:

+ +
$ bin/rails s
+ +

Visit localhost:3000/api/v1/employees. You should see a JSONAPI Response:

+ +


+ +

jsonapi

+ +


+ +

If you find the payload a little intimidating, add .json to the URL for a more traditional response:

+ +


+ +

json

+ +


+ +

There’s .xml, too:

+ +


+ +

xml

+ +


+ +

These are all different renderings of the same EmployeeResource.

+ +

Resources are comprised of Attributes:

+ +
# app/resources/employee_resource.rb
+attribute :first_name, :string
+ +

Each attribute defines behavior for:

+ +
    +
  • Reading (display)
  • +
  • Writing
  • +
  • Sorting
  • +
  • Filtering
  • +
  • Fieldsets
  • +
+ +

Let’s start with simple display, turning first_name into all capital +letters:

+ +
# app/resources/employee_resource.rb
+attribute :first_name, :string do
+  # @object is your model instance
+  @object.first_name.upcase
+end
+ +

Which gives us:

+ +

+ +


+ +

This is the most important thing to understand about Resources: they are +just a collection of defaults, all of which can be overridden. In other +words:

+ +
attribute :first_name
+
+# is the same as
+
+attribute :first_name do
+  @object.first_name
+end
+ +

We’ll go into further Resource customizations over the course of this +tutorial. For now, let’s just verify our out-of-the-box defaults:

+ +
    +
  • Sort by first_name ascending: http://localhost:3000/api/v1/employees?sort=first_name
  • +
  • Sort by first_name descending: http://localhost:3000/api/v1/employees?sort=-first_name
  • +
  • Return only age and created_at in the response: http://localhost:3000/api/v1/employees?fields[employees]=age,created_at
  • +
  • Filter on first_name: +
      +
    • Case-insensitive: http://localhost:3000/api/v1/employees?filter[first_name]=bob
    • +
    • Case-sensitive: http://locahost:3000/api/v1/employees?filter[first_name][eql]=Bob
    • +
    • Prefix: http://localhost:3000/api/v1/employees?filter[first_name][prefix]=b
    • +
    • Suffix: http://localhost:3000/api/v1/employees?filter[first_name][suffix]=ob
    • +
    • Contains: http://localhost:3000/api/v1/employees?filter[first_name][match]=o
    • +
    +
  • +
  • Filter on age: +
      +
    • Equal: http://localhost:3000/api/v1/employees?filter[age]=39
    • +
    • Greater Than: http://localhost:3000/api/v1/employees?filter[age][gt]=39
    • +
    • Greater Than or Equal To: http://localhost:3000/api/v1/employees?filter[age][gte]=39
    • +
    • Less Than: http://localhost:3000/api/v1/employees?filter[age][lt]=65
    • +
    • Less Than or Equal To: http://localhost:3000/api/v1/employees?filter[age][lte]=65
    • +
    +
  • +
  • Paginate +
      +
    • 10 per page: http://localhost:3000/api/v1/employees?page[size]=10
    • +
    • 5 per page, third page: +http://localhost:3000/api/v1/employees?page[number]=3
    • +
    +
  • +
+ +

Write operations are easiest to verify with integration tests, which +were created when we generated our Resource. Let’s take a look at the +test for creating Employees:

+ +
# spec/api/v1/employees/create_spec.rb
+
+RSpec.describe "employees#create", type: :request do
+  subject(:make_request) do
+    jsonapi_post "/api/v1/employees", payload
+  end
+
+  describe 'basic create' do
+    let(:payload) do
+      {
+        data: {
+          type: 'employees',
+          attributes: {
+            # ... your attrs here
+          }
+        }
+      }
+    end
+
+    it 'works' do
+      expect(EmployeeResource).to receive(:build).and_call_original
+      expect {
+        make_request
+      }.to change { Employee.count }.by(1)
+      expect(response.status).to eq(201)
+    end
+  end
+end
+ +

This is an API Spec, which tests high-level end-to-end functionality. We +know that if our API receives a POST with the given payload, an +Employee will be created and a 201 response code will be returned.

+ +

API specs are high-level - often they won’t be changed past this initial +boilerplate. For testing logic, use a Resource Spec. These +integration tests hit the database and run logic, but operate without a +specific request or response:

+ +
# spec/api/v1/employees/create_spec.rb
+RSpec.describe EmployeeResource, type: :resource do
+  describe 'creating' do
+    let(:payload) do
+      {
+        data: {
+          type: 'employees',
+          attributes: {
+            first_name: 'Jane'
+            last_name: 'Doe'
+            age: 30
+          }
+        }
+      }
+    end
+
+    let(:instance) do
+      EmployeeResource.build(payload)
+    end
+
+    it 'works' do
+      expect {
+        expect(instance.save).to eq(true)
+      }.to change { Employee.count }.by(1)
+      employee = Employee.last
+      expect(employee.first_name).to eq('Jane')
+      expect(employee.last_name).to eq('Doe')
+      expect(employee.age).to eq(30)
+    end
+  end
+end
+ +

In other words: API specs test Endpoints (request, response, middleware, +etc), Resource specs test only the Resource (actual application logic). +Read more in our Testing Guide.

+ +

Before we run these specs, we need to edit our factories to ensure +dynamic, randomized data. Let’s change this:

+ +
# spec/factories/employee.rb
+
+FactoryBot.define do
+  factory :employee do
+    first_name { "MyString" }
+    last_name { "MyString" }
+    age { 1 }
+  end
+end
+ +

To

+ +
# spec/factories/employee.rb
+
+FactoryBot.define do
+  factory :employee do
+    first_name { Faker::Name.first_name }
+    last_name { Faker::Name.last_name }
+    age { rand(20.80) }
+  end
+end
+ +

Now undo the capitalization change to attribute :first_name, and run the generated specs:

+ +
$ bin/rspec
+ +

You’ll see 11 tests pass, with 3 pending. One of the pending specs was +autogenerated by rails - you can delete spec/models/employee_spec.rb +for now.

+ +

That leaves us with two “update” specs. These are marked pending so you +can manage the data yourself. Follow the comments in these specs to add +attributes and get them passing.

+ +
+ + + +
+
+
+ + + + + + diff --git a/website/static/1.13/tutorial/step_2.html b/website/static/1.13/tutorial/step_2.html new file mode 100644 index 00000000..d34dce27 --- /dev/null +++ b/website/static/1.13/tutorial/step_2.html @@ -0,0 +1,481 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +

Step 2: Has Many

+ +
+

View the Code

+
+ +

We’ll be adding the database table positions:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
idemployee_idtitleactivehistorical_indexcreated_atupdated_at
1900Engineertrue12018-09-042018-09-04
2900Interntrue22018-09-042018-09-04
3800Managertrue12018-09-042018-09-04
+ +

Because this table tracks all historical positions, we have the +historical_index column. This tells the order the employee moved +through each position, where 1 is most recent.

+ +

The Rails Stuff 🚂

+ +

Generate the Position model:

+ +
$ bin/rails g model Position title:string active:boolean historical_index:integer employee:belongs_to
+$ bin/rails db:migrate
+ +

Update the Employee model with the association, too:

+ +
# app/models/employee.rb
+has_many :positions
+ +

And update our seed data:

+ +
# db/seeds.rb
+[Employee, Position].each(&:delete_all)
+
+100.times do
+  employee = Employee.create! first_name: Faker::Name.first_name,
+    last_name: Faker::Name.last_name,
+    age: rand(20..80)
+
+  (1..2).each do |i|
+    employee.positions.create! title: Faker::Job.title,
+      historical_index: i,
+      active: i == 1
+  end
+end
+ +
$ bin/rails db:seed
+ +

The Graphiti Stuff 🎨

+ +

Let’s start by running the same command as before to create +PositionResource:

+ +
$ bin/rails g graphiti:resource Position title:string active:boolean
+ +

We’ll need to add the association, just like ActiveRecord:

+ +
# app/resources/employee_resource.rb
+has_many :positions
+ +

…and a corresponding filter:

+ +
# app/resources/position_resource.rb
+filter :employee_id, :integer
+ +

If you visit /api/v1/employees, you’ll see a number of HTTP +Links +that allow lazy-loading positions. Or, if you visit +/api/v1/employees?include=positions, you’ll load the employees and +positions in a single request. We’ll dig a bit deeper into this logic +in the section below.

+ +

Before we get there, let’s revisit the historical_index column. For now, let’s +treat this as an implementation detail that the API should not expose - +let’s say we want to support sorting on this attribute but nothing else:

+ +
attribute :historical_index, :integer, only: [:sortable]
+ +

We’re almost done, but if you run your tests you’ll see two outstanding +errors. This is because Rails 5 belongs_to associations are required by +default. We can’t save a Position without its corresponding Employee.

+ +

We can solve this in three ways:

+ +
    +
  • Turn this off globally, with config.active_record.belongs_to_required_by_default. You may want to do this in test-mode only.
  • +
  • Turn this off for the specific association: belongs_to :employee, +optional: true.
  • +
  • Associate an Employee as part of the API request.
  • +
+ +

We’ll take for the last option. Look at +spec/resources/position/writes_spec.rb:

+ +
RSpec.describe PositionResource, type: :resource do
+  describe 'creating' do
+    let(:payload) do
+      {
+        data: {
+          type: 'positions',
+          attributes: { }
+        }
+      }
+    end
+
+    let(:instance) do
+      PositionResource.build(payload)
+    end
+
+    it 'works' do
+      expect {
+        expect(instance.save).to eq(true)
+      }.to change { Position.count }.by(1)
+    end
+  end
+end
+ +

When running our tests, let’s make sure the historical_index column +reflects the order we created the positions. This code recalculates +everything after a record is saved:

+ +
# spec/factories/position.rb
+FactoryBot.define do
+  factory :position do
+    employee
+
+    title { Faker::Job.title }
+
+    after(:create) do |position|
+      unless position.historical_index
+        scope = Position
+          .where(employee_id: position.employee.id)
+          .order(created_at: :desc)
+        scope.each_with_index do |p, index|
+          p.update_attribute(:historical_index, index + 1)
+        end
+      end
+    end
+  end
+end
+ +

Let’s associate an Employee. Start by seeding the data:

+ +
let!(:employee) { create(:employee) }
+ +

And associate via relationships:

+ +
let(:payload) do
+  {
+    data: {
+      type: 'positions',
+      attributes: { },
+      relationships: {
+        employee: {
+          data: {
+            id: employee.id.to_s,
+            type: 'employees'
+          }
+        }
+      }
+    }
+  }
+end
+ +

To ensure the PositionResource will process this relationship, the +last step is to add it:

+ +
# app/resources/position_resource.rb
+belongs_to :employee
+ +

This will associate the Position to the Employee as part of the +creation process. The test should now pass - make the same change to +spec/api/v1/positions/create_spec.rb to get a fully-passing test +suite.

+ +

Digging Deeper 🧐

+ +

Why did we need the employee_id filter above? To explain that, let’s dive deeper into the logic connecting Resources.

+ +

If you hit /api/v1/employees, you’ll see a number of +Links in the +response. These are useful for lazy-loading, but the same logic +applies to eager loading. Let’s take a look at a Link to see how these +Resources connect together:

+ +
{
+  ...
+  relationships: {
+    positions: {
+      links: {
+        related: "http://localhost:3000/api/v1/positions?filter[employee_id]=1"
+      }
+    }
+  }
+  ...
+}
+ +

The salient bit: /positions?filter[employee_id]=1. In other words, +fetch all Positions for the given Employee id.That means, whether we’re lazy-loading data in separate requests or +eager-loading in a single request, the same logic fires +under-the-hood:

+ +
PositionResource.all({
+  filter: { employee_id: 1 }
+})
+ +

This means we need filter :employee_id, :integer to satisfy the query.

+ +

We can customize the logic connecting Resources in a few different +ways. First some simple options:

+ +
has_many :positions, foreign_key: :emp_id, primary_key: :eid
+ +

So far so good. The logic, and corresponding Link, both update as you’d +expect (though we’d of course need a corresponding filter +:emp_id, :integer on PositionResource).

+ +

Those options are just simple versions of parameter customization. +You can customize parameters connecting Resources with the params block:

+ +
has_many :positions do
+  params do |hash, employees|
+    hash[:filter] # => { employee_id: employees.map(&:id) }
+    hash[:filter][:active] = true
+    hash[:sort] = '-created_at'
+  end
+end
+ +

Customizing these params affects the Link as well as the eager-load +logic. Remember the parameters here should reflect the JSON:API +specification, or anything PositionResource.all accepts.

+ +

These are the most common options, but there’s a bunch more. Check +out the Resource Relationships Guide to dig even deeper.

+ +
+ + + +
+
+
+ + + + + + diff --git a/website/static/1.13/tutorial/step_3.html b/website/static/1.13/tutorial/step_3.html new file mode 100644 index 00000000..f22579d2 --- /dev/null +++ b/website/static/1.13/tutorial/step_3.html @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +

Step 3: Belongs To

+ +
+

View the Diff

+
+ +

We’ll be adding the database table departments:

+ + + + + + + + + + + + + + + + + + + + + + +
idname
1Engineering
2Safety
3QA
+ +

We’ll also be adding a department_id:integer foreign key column to +the positions table.

+ +

The Rails Stuff 🚂

+ +

Generate the Department model:

+ +
$ bin/rails g model Department name:string
+ +

To add the foreign key to positions:

+ +
$ bin/rails g migration add_department_id_to_positions
+ +
class AddDepartmentIdToPositions < ActiveRecord::Migration[5.2]
+  def change
+    add_foreign_key :positions, :departments
+  end
+end
+ +

Update the database:

+ +
$ bin/rails db:migrate
+ +

Update our seed file:

+ +
[Employee, Position, Department].each(&:delete_all)
+
+engineering = Department.create! name: 'Engineering'
+safety = Department.create! name: 'Safety'
+qa = Department.create! name: 'QA'
+departments = [engineering, safety, qa]
+
+100.times do
+  employee = Employee.create! first_name: Faker::Name.first_name,
+    last_name: Faker::Name.last_name,
+    age: rand(20..80)
+
+  (1..2).each do |i|
+    employee.positions.create! title: Faker::Job.title,
+      historical_index: i,
+      active: i == 1,
+      department: departments.sample
+  end
+end
+ +

Make sure to update spec/factories/departments.rb with randomized +data. Then, since this is also a required relationship, update +spec/factories/positions.rb to always seed a department when we ask to +create a position:

+ +
factory :position do
+  employee
+  department
+
+  # ... code ...
+end
+ +

The Graphiti Stuff 🎨

+ +

You should be used to this by now:

+ +
bin/rails g graphiti:resource Department name:string
+ +

Add the association:

+ +
# app/resources/position_resource.rb
+belongs_to :department
+ +

And review the end of Step 2 to get all your specs +passing (add the department to the request payload). Practice makes perfect!

+ +

Digging Deeper 🧐

+ +

Note that we didn’t need a filter like we did in step two. That’s +because the primary key connecting the Resources is id by +default. In other words, the Link would be something like:

+ +
/departments?filter[id]=1
+ +

Which we get out-of-the-📦

+ +

But remember, you can customize these relationships just like the +previous has_many section.

+ +
+ + + +
+
+
+ + + + + + diff --git a/website/static/1.13/tutorial/step_4.html b/website/static/1.13/tutorial/step_4.html new file mode 100644 index 00000000..25674b5b --- /dev/null +++ b/website/static/1.13/tutorial/step_4.html @@ -0,0 +1,318 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +

Step 4: Customizing Queries

+ +
+

View the Diff

+
+ +

So far, we’ve done fairly straightforward queries. If a user filters on +first_name:

+ +

/api/v1/employees?filter[first_name]=Foo

+ +

We’ll query the equivalent database column:

+ +
Employee.where(first_name: 'Foo')
+ +

But what if there’s more complex logic? Let’s say we want to sort +Employees on their title - which comes from the positions table. +How would that work?

+ +

The Rails Stuff 🚂

+ +

First, we need to get data for an Employee’s current position. +Let’s start by defining what current means

+ +
# app/models/position.rb
+scope :current, -> { where(historical_index: 1) }
+ +
+

See the ActiveRecord Scopes documentation if you’re unfamiliar with this concept.

+
+ +

Reference this scope in a new association:

+ +
has_one :current_position,
+  -> { current },
+  class_name: 'Position'
+ +

Before moving on, let’s review what we need to do. The ActiveRecord +code for sorting Employees on their current position’s title would be:

+ +
Employee.joins(:current_position).merge(Position.order(title: :asc))
+ +

Let’s wire this up to Graphiti:

+ +

The Graphiti Stuff 🎨

+ +

We’re only going to sort and filter on the title attribute - +never display or persist. So start by defining the attribute as such:

+ +
attribute :title, :string, only: [:filterable, :sortable]
+ +

Then the sort DSL to place our custom query:

+ +
# app/resources/employee_resource.rb
+sort :title do |scope, direction|
+  scope.joins(:current_position).merge(Position.order(title: direction))
+end
+ +

That’s it! When a request to sort on the title comes in, we’ll alter our +scope to join on the positions table, and order based on the current +position title.

+ +

The solution for filtering is similar:

+ +
# app/resources/employee_resource.rb
+filter :title do
+  eq do |scope, value|
+    scope.joins(:current_position).merge(Position.where(title: value))
+  end
+end
+ +

We can now filter on title:

+ +

/api/v1/employees?filter[title]=Foo

+ +

Let’s do one more example - how would we order Employees by department +name? We could start the same way:

+ +
attribute :department_name, :string, only: [:sortable]
+ +

But if we’re only sorting, this is actually redundant. Whenever we +use the sort or filter DSL, we’re just creating a sort-only or +filter-only attribute under the hood. So let’s define everything in one +shot:

+ +
sort :department_name, :string do |scope, value|
+  scope.joins(current_position: :department)
+    .merge(Department.order(name: value))
+end
+ +

Remember: you only need to pass the type as the second argument when an +attribute doesn’t already exist. And if you ever get an error saying +something is unfilterable or unsortable, check to see if you’ve already +defined a filter-only or sort-only attribute using these methods.

+ +

Digging Deeper 🧐

+ +

There’s a critical part of Graphiti that makes everything easier: start +by imagining it doesn’t exist.

+ +

In other words, the meat of the logic above had nothing to do with +Graphiti code - we’re simply “wiring up” independent ActiveRecord +queries. If you’re ever confused about query logic, get things working +without Graphiti first.

+ +

We could have changed the above to ActiveRecord scopes like +.order_by_title(title), making the wiring code even simpler. Consider +doing this when the logic is reusable or particlar complex, but be aware +of the tradeoffs of double-testing units.

+ +
+ + + +
+
+
+ + + + + + diff --git a/website/static/1.13/tutorial/step_5.html b/website/static/1.13/tutorial/step_5.html new file mode 100644 index 00000000..694848d5 --- /dev/null +++ b/website/static/1.13/tutorial/step_5.html @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +

Step 5: Has One

+ +
+

View the Diff

+
+ +

In the last step, we introduced the concept of a “current position” to +the model layer. Let’s now expose that relationship to the API.

+ +

The Rails Stuff 🚂

+ +

We already defined a Position.current scope that we’ll re-use - +let’s just make a small tweak to support the opposite use case as +well:

+ +
scope :current, ->(bool) {
+  clause = { historical_index: 1 }
+  bool ? where(clause) : where.not(clause)
+}
+ +

The Graphiti Stuff 🎨

+ +

You might already have an idea how this might work from the prior step

+
    +
  • we’ll use the params block to customize the relationship. The +has_one macro ensures the result is treated as a single object and +not an array.
  • +
+ +
# app/resources/employee_resource.rb
+has_one :current_position, resource: PositionResource do
+  params do |hash|
+    hash[:filter][:current] = true
+  end
+end
+ +

Which means we’ll have to implement that filter - re-using the +ActiveRecord scope we already defined!

+ +
filter :current, :boolean do
+  eq { |scope, value| scope.current(value) }
+end
+ +

Digging Deeper 🧐

+ +

In this example, we’re able to return only a single record because we +have a historical_index column. If this column didn’t exist - maybe +we’re just ordering on created_at and taking the first record - we’d +have a problem. What if we were loading 20 employees and wanted the +current_position of each - what SQL would limit the resultset +correctly?

+ +

We call this a faux has_one and there’s nothing easily done here. Graphiti will ensure only one record +is returned by the API, but the query will take longer and loading extra +records will eat memory. If there are lots of records in the +association, look into adding a column like historical_index.

+ +
+ + + +
+
+
+ + + + + + diff --git a/website/static/1.13/tutorial/step_6.html b/website/static/1.13/tutorial/step_6.html new file mode 100644 index 00000000..e34ebc2a --- /dev/null +++ b/website/static/1.13/tutorial/step_6.html @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +

Step 6: Customizing Writes

+ +
+

View the Diff

+
+ +

When we ran the generators (and created a blank Resource class), we got the ability to create, update, and destroy resources for free. You can turn off this behavior with self.read_only = true. Or for relationships: has_many :positions, writable: false.

+ +

But in RESTful APIs, it’s super common for persistence operations to +have side effects - that’s how we avoid extraneous verbs and +inconsistent patterns.

+ +

In a prior step, we updated position Factory to automatically reorder +the historical_index: when a new record comes in, all the prior +values need to change. This step will show how to add that behavior to +our API, using hooks that work for a variety of scenarios: sending +emails, checking authorization roles, queuing delayed jobs, and more.

+ +

The Rails Stuff 🚂

+ +

Previously, we put the logic that re-ordered the historical_index +column in the position factory. Let’s move that to the model so our +tests and API can share the same logic:

+ +
# app/models/position.rb
+def self.reorder!(employee_id)
+  scope = Position.where(employee_id: employee_id).order(created_at: :desc)
+  scope.each_with_index do |p, index|
+    p.update_attribute(:historical_index, index + 1)
+  end
+end
+ +
# spec/factories/positions.rb
+# ... code ...
+after(:create) do |position|
+  unless position.historical_index
+    Position.reorder!(position.employee.id)
+  end
+end
+ +

The Graphiti Stuff 🎨

+ +

All Graphiti updates happen within a transaction. We want to insert our +code right before that transaction closes - after the graph of objects +has been persisted and validations have passed. To do that, we’ll use +the before_commit hook:

+ +
before_commit only: [:create, :destroy] do |position|
+  Position.reorder!(position.employee_id)
+end
+ +

Again, note that the Position.reorder! code existed independent of our +API, and was re-used in our factory.

+ +

Digging Deeper 🧐

+ +

Resources come with Lifecycle Hooks, similar to ActiveRecord Callbacks.

+ +

Those callbacks have gotten a bad reputation. This is because your Model +can be - is supposed to be - used in a variety of contexts across your +application. Some of those contexts will want a given callback to fire, +others will not, and accomodating the conditionals gets hairy. This is +why many developers move that functionality into Service Objects.

+ +

But Resource callbacks don’t have the same problem - they only fire in +the context of your API, and can be associated to a single endpoint. You +can still use Service Objects if you’d like; Graphiti callbacks simply wire them up.

+ +
+ + + +
+
+
+ + + + + + diff --git a/website/static/1.13/tutorial/step_7.html b/website/static/1.13/tutorial/step_7.html new file mode 100644 index 00000000..ac709046 --- /dev/null +++ b/website/static/1.13/tutorial/step_7.html @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +

Step 7: Many to Many

+ +
+

View the Diff

+
+ +

Let’s add a Team relationship: a Team can have many Employees, an +Employee can have many Teams. Let’s also say a Team belongs to a +Department.

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
iddepartment_idname
11The A Team
21The B Team
32The C Team
+ +

To satisfy this many-to-many use case, we’ll need a join model, +TeamMembership:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
idteam_idemployee_id
111
221
332
+ +

The Rails Stuff 🚂

+ +
$ bin/rails g model Team name:string department:belongs_to
+$ bin/rails g model TeamMembership employee:belongs_to team:belongs_to
+$ bin/rails db:migrate
+ +

Graphiti supports has_many :through:

+ +
# app/models/employee.rb
+has_many :team_memberships
+has_many :teams, through: :team_memberships
+ +
# app/models/department.rb
+has_many :teams
+ +
class Team < ApplicationRecord
+  belongs_to :department
+  has_many :team_memberships
+  has_many :employees, through: :team_memberships
+end
+ +
class TeamMembership < ApplicationRecord
+  belongs_to :team
+  belongs_to :employee
+end
+ +

Finally, we’ll need a new seed file to handle these new associations:

+ +
[
+  Employee,
+  Position,
+  Department,
+  TeamMembership,
+  Team
+].each(&:delete_all)
+
+departments = []
+def create_department(name)
+  dept = Department.create! name: name
+  dept.teams.create!(name: 'Engineering Team B')
+  dept.teams.create!(name: 'Engineering Team C')
+  dept
+end
+
+departments << create_department('Engineering')
+departments << create_department('Safety')
+departments << create_department('QA')
+
+100.times do
+  employee = Employee.create! first_name: Faker::Name.first_name,
+    last_name: Faker::Name.last_name,
+    age: rand(20..80)
+
+  (1..2).each do |i|
+    employee.positions.create! title: Faker::Job.title,
+      historical_index: i,
+      active: i == 1,
+      department: departments.sample
+  end
+
+  employee.teams << employee.positions[0].department.teams.sample
+end
+ +

The Graphiti Stuff 🎨

+ +
$ bin/rails g graphiti:resource Team name:string
+ +

Let’s flesh out our TeamResource:

+ +
# app/resources/team_resource.rb
+class TeamResource < ApplicationResource
+  attribute :department_id, :integer, only: [:filterable]
+  attribute :name, :string
+
+  belongs_to :department
+  many_to_many :employees
+end
+ +

The trick here is the many_to_many relationship. Let’s add the reverse +as well:

+ +
# app/resources/employee_resource.rb
+many_to_many :teams
+ +

And for good measure:

+ +
# app/resources/department_resource.rb
+has_many :teams
+ +

We can now get all the usual functionality: fetch Employees and their +Teams in a single request (or vice versa).

+ +

Digging Deeper 🧐

+ +

The many_to_many relationship is the only one where Graphiti modifies +a separate Resource “under the hood”. When we said many_to_many +:employees, the EmployeeResource got a team_id filter, and +many_to_many :teams created an employee_id filter on TeamResource.

+ +

This is because the logic is more complex than the default use case. We +don’t have a simple WHERE clause; we need to join tables and look at +the appropriate primary/foreign keys. If the name of your API +association doesn’t match the name of your ActiveRecord association, try +has_many :things, as: :my_activerecord_relationship to make the +introspection work correctly - or, write your own filter.

+ +

Sometimes you’ll have multiple levels of has_many :through. In this +case, a simple many_to_many isn’t enough - check out our Hopping +Relationships Cookbook.

+ +

Think hard before reaching for many_to_many. Imagine one Team is the +“primary” Team for an Employee. We’d add a primary boolean column to +the team_memberships table…but that table isn’t exposed to the API! +Consider if there’s a hidden domain concept there.

+ +
+ + + +
+
+
+ + + + + + diff --git a/website/static/1.13/tutorial/step_8.html b/website/static/1.13/tutorial/step_8.html new file mode 100644 index 00000000..4271174f --- /dev/null +++ b/website/static/1.13/tutorial/step_8.html @@ -0,0 +1,316 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +

Step 8: Polymorphic Relationships

+ +
+

View the Diff

+
+ +

Let’s introduce the concept of a Note. A Note can belong to a +Department, an Employee, or a Team. For this, we’ll need to introduce +the concept of polymorphism.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
idnotable_idnotable_typebody
11EmployeeA Sample Note!
21DepartmentAnother Sample Note!
31TeamA Third Sample Note!
+ +

The Rails Stuff 🚂

+ +
$ rails generate model Note notable:references{polymorphic}:index
+$ bin/rails db:migrate
+ +

Make sure to add the corresponding model relationships:

+ +
# app/models/employee.rb
+has_many :notes, as: :notable
+# app/models/team.rb
+has_many :notes, as: :notable
+# app/models/department.rb
+has_many :notes, as: :notable
+
+# app/models/note.rb
+belongs_to :notable, polymorphic: true
+ +

Finally, make sure to edit your seed file - check out the diff to see the necessary adjustments.

+ +

The Graphiti Stuff 🎨

+ +
$ bin/rails g graphiti:resource Note body:string
+ +

Let’s create our NoteResource:

+ +
class NoteResource < ApplicationResource
+  attribute :body, :string
+
+  filter :notable_id, :integer
+  filter :notable_type, :string, allow: %w(Employee Department Team)
+
+  polymorphic_belongs_to :notable do
+    group_by(:notable_type) do
+      on(:Employee)
+      on(:Team)
+      on(:Department)
+    end
+  end
+end
+ +

And corresponding associations:

+ +
# app/resources/employee_resource.rb
+polymorphic_has_many :notes, as: :notable
+# app/resources/team_resource.rb
+polymorphic_has_many :notes, as: :notable
+# app/resources/department_resource.rb
+polymorphic_has_many :notes, as: :notable
+ +

Digging Deeper 🧐

+ +

When defining a polymorphic relationship for our API, we’re saying “grab +all the parent records, group them by a type column, and execute +different queries for each type”. This way records with notable_type == +'Employee' can hit the employees table, but records with +notable_type == 'Department' could in theory load from a different API +altogether.

+ +

Each of the on lines defines a new belongs_to association. That +means you can customize just like always:

+ +
on(:Team).belongs_to :team, resource: SomeCustomTeamResource do
+  # assign {}
+  # link {}
+  # ... etc ...
+end
+ +
+ + + +
+
+
+ + + + + + diff --git a/website/static/1.13/tutorial/step_9.html b/website/static/1.13/tutorial/step_9.html new file mode 100644 index 00000000..b4a21108 --- /dev/null +++ b/website/static/1.13/tutorial/step_9.html @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + Graphiti + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+ +

Step 9: Polymorphic Resources

+ +
+

View the Diff

+
+ +

In the last step, we covered polymorphic relationships: a single +relationship can point to many different Resources. Polymorphic +Resources are the same concept, without an association: a single +Resource can resolve to many different sub-Resources. It’s a very similar +to Single-Table Inheritance in ActiveRecord.

+ +

To illustrate this, we’ll add a tasks table and corresponding Task +superclass. Each record in this table will resolve to one of Bug, +Epic, or Feature.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
idmilestone_idtypetitle
1nullBugIncorrect Value!
2nullFeatureBuild great stuff!
31EpicBuild TONS of great stuff!
+ +

Why not just stick with a single Task model? Because each of these +types has specific behavior: only Features have a points attribute, +and only Epics have a milestones relationship.

+ +

The Rails Stuff 🚂

+ +

Let’s create our Task model:

+ +
$ bin/rails g model Task employee:belongs_to team:belongs_to type:string
+title:string
+$ bin/rails db:migrate
+ +

And create models to reflect our STI logic:

+ +
# app/models/task.rb
+class Task < ApplicationRecord
+  TYPES = %w(Bug Feature Epic)
+
+  belongs_to :team, optional: true
+  belongs_to :employee, optional: true
+end
+
+# app/models/bug.rb
+class Bug < Task
+end
+
+# app/models/feature.rb
+class Feature < Task
+end
+
+# Only Epics have Milestones
+# app/models/epic.rb
+class Epic < Task
+  has_many :milestones
+end
+
+# app/models/milestone.rb
+class Milestone < ApplicationRecord
+  belongs_to :epic
+end
+ +

Add the association:

+ +
# app/models/team.rb
+has_many :tasks
+has_many :bugs
+has_many :features
+has_many :epics
+
+# app/models/employee.rb
+has_many :tasks
+has_many :bugs
+has_many :features
+has_many :epics
+ +

Finally view the diff to edit your seeds.rb file.

+ +

The Graphiti Stuff 🎨

+ +

Start by creating our Resource as normal:

+ +
$ bin/rails g graphiti:resource Task title:string
+ +

Now edit to support polymorphism and associations:

+ +
class TaskResource < ApplicationResource
+  self.polymorphic = %w(FeatureResource BugResource EpicResource)
+
+  attribute :employee_id, :integer, only: [:filterable]
+  attribute :team_id, :integer, only: [:filterable]
+  attribute :title, :string
+
+  belongs_to :employee
+  belongs_to :team
+end
+ +

The point of this was to show how responses could be specific to type, +so let’s customize Features:

+ +
class FeatureResource < TaskResource
+  attribute :points, :integer do
+    rand(20)
+  end
+end
+ +

Only Epics have milestones, but let’s support those as well:

+ +
$ bin/rails g graphiti:resource Milestone name:string
+ +
class MilestoneResource < ApplicationResource
+  attribute :epic_id, :integer, only: [:filterable]
+  attribute :name, :string
+
+  # Customize the link to the Tasks endpoint, as we
+  # didn't create an Epics endpoint
+  belongs_to :epic do
+    link do |milestone|
+      helpers = Rails.application.routes.url_helpers
+      helpers.task_url(milestone.epic_id)
+    end
+  end
+end
+ +

Digging Deeper 🧐

+ +

We can now resolve Tasks, either as a relationship or through the +/tasks endpoint directly. When Task is type 'Feature' it will have +an extra attribute of points. When it’s an Epic, it will have an +additional relationship Milestone.

+ +

Graphiti is smart enough to fetch the appropriate relationships. A hit +to /tasks?include=milestones will only query for milestones when the +resulting Task records are Epics.

+ +


+
+
+
+

+ +
+ +
+
+
+ + + + + + diff --git a/website/static/CNAME b/website/static/CNAME new file mode 100644 index 00000000..b91624f3 --- /dev/null +++ b/website/static/CNAME @@ -0,0 +1 @@ +graphiti.dev diff --git a/website/static/assets/img/error_payload.png b/website/static/assets/img/error_payload.png new file mode 100644 index 00000000..da986058 Binary files /dev/null and b/website/static/assets/img/error_payload.png differ diff --git a/website/static/assets/img/legacy/legacy-0378a3bb39.png b/website/static/assets/img/legacy/legacy-0378a3bb39.png new file mode 100644 index 00000000..40562b6d Binary files /dev/null and b/website/static/assets/img/legacy/legacy-0378a3bb39.png differ diff --git a/website/static/assets/img/legacy/legacy-05bbd3e5fd.png b/website/static/assets/img/legacy/legacy-05bbd3e5fd.png new file mode 100644 index 00000000..fe88aa86 Binary files /dev/null and b/website/static/assets/img/legacy/legacy-05bbd3e5fd.png differ diff --git a/website/static/assets/img/legacy/legacy-07aa104495.png b/website/static/assets/img/legacy/legacy-07aa104495.png new file mode 100644 index 00000000..1b2d361b Binary files /dev/null and b/website/static/assets/img/legacy/legacy-07aa104495.png differ diff --git a/website/static/assets/img/legacy/legacy-0c75a16b3a.gif b/website/static/assets/img/legacy/legacy-0c75a16b3a.gif new file mode 100644 index 00000000..b081e25f Binary files /dev/null and b/website/static/assets/img/legacy/legacy-0c75a16b3a.gif differ diff --git a/website/static/assets/img/legacy/legacy-3076df6209.png b/website/static/assets/img/legacy/legacy-3076df6209.png new file mode 100644 index 00000000..1ce5c394 Binary files /dev/null and b/website/static/assets/img/legacy/legacy-3076df6209.png differ diff --git a/website/static/assets/img/legacy/legacy-7f6889bc89.png b/website/static/assets/img/legacy/legacy-7f6889bc89.png new file mode 100644 index 00000000..d98e74af Binary files /dev/null and b/website/static/assets/img/legacy/legacy-7f6889bc89.png differ diff --git a/website/static/assets/img/legacy/legacy-a2cc4363c3.png b/website/static/assets/img/legacy/legacy-a2cc4363c3.png new file mode 100644 index 00000000..2243be0b Binary files /dev/null and b/website/static/assets/img/legacy/legacy-a2cc4363c3.png differ diff --git a/website/static/assets/img/legacy/legacy-f67cfa89ab.png b/website/static/assets/img/legacy/legacy-f67cfa89ab.png new file mode 100644 index 00000000..081f20c1 Binary files /dev/null and b/website/static/assets/img/legacy/legacy-f67cfa89ab.png differ diff --git a/website/static/assets/img/meta_total_count.png b/website/static/assets/img/meta_total_count.png new file mode 100644 index 00000000..1cfcaf7c Binary files /dev/null and b/website/static/assets/img/meta_total_count.png differ diff --git a/website/static/img/docusaurus-social-card.jpg b/website/static/img/docusaurus-social-card.jpg new file mode 100644 index 00000000..ffcb4482 Binary files /dev/null and b/website/static/img/docusaurus-social-card.jpg differ diff --git a/website/static/img/docusaurus.png b/website/static/img/docusaurus.png new file mode 100644 index 00000000..f458149e Binary files /dev/null and b/website/static/img/docusaurus.png differ diff --git a/website/static/img/favicon.ico b/website/static/img/favicon.ico new file mode 100644 index 00000000..c00f6af7 Binary files /dev/null and b/website/static/img/favicon.ico differ diff --git a/website/static/img/logo.png b/website/static/img/logo.png new file mode 100644 index 00000000..5ed5ca09 Binary files /dev/null and b/website/static/img/logo.png differ diff --git a/website/static/img/logo.svg b/website/static/img/logo.svg new file mode 100644 index 00000000..9db6d0d0 --- /dev/null +++ b/website/static/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/website/static/img/undraw_docusaurus_mountain.svg b/website/static/img/undraw_docusaurus_mountain.svg new file mode 100644 index 00000000..af961c49 --- /dev/null +++ b/website/static/img/undraw_docusaurus_mountain.svg @@ -0,0 +1,171 @@ + + Easy to Use + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/img/undraw_docusaurus_react.svg b/website/static/img/undraw_docusaurus_react.svg new file mode 100644 index 00000000..94b5cf08 --- /dev/null +++ b/website/static/img/undraw_docusaurus_react.svg @@ -0,0 +1,170 @@ + + Powered by React + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/website/static/img/undraw_docusaurus_tree.svg b/website/static/img/undraw_docusaurus_tree.svg new file mode 100644 index 00000000..d9161d33 --- /dev/null +++ b/website/static/img/undraw_docusaurus_tree.svg @@ -0,0 +1,40 @@ + + Focus on What Matters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +