diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..325bfc0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,51 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. + +# Ignore git directory. +/.git/ +/.gitignore + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all default key files. +/config/master.key +/config/credentials/*.key + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/.keep + +# Ignore assets. +/node_modules/ +/app/assets/builds/* +!/app/assets/builds/.keep +/public/assets + +# Ignore CI service files. +/.github + +# Ignore Kamal files. +/config/deploy*.yml +/.kamal + +# Ignore development files +/.devcontainer + +# Ignore Docker-related files +/.dockerignore +/Dockerfile* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8dc4323 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored +config/credentials/*.yml.enc diff=rails_credentials +config/credentials.yml.enc diff=rails_credentials diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f0527e6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: +- package-ecosystem: bundler + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..abb548b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + pull_request: + push: + branches: [ main ] + +jobs: + scan_ruby: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Scan for common Rails security vulnerabilities using static analysis + run: bin/brakeman --no-pager + + scan_js: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Scan for security vulnerabilities in JavaScript dependencies + run: bin/importmap audit + + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Lint code for consistent style + run: bin/rubocop -f github + + test: + runs-on: ubuntu-latest + + # services: + # redis: + # image: redis + # ports: + # - 6379:6379 + # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Install packages + run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git pkg-config google-chrome-stable + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Run tests + env: + RAILS_ENV: test + # REDIS_URL: redis://localhost:6379/0 + run: bin/rails db:test:prepare test test:system + + - name: Keep screenshots from failed system tests + uses: actions/upload-artifact@v4 + if: failure() + with: + name: screenshots + path: ${{ github.workspace }}/tmp/screenshots + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index b512c09..f92525c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,34 @@ -node_modules \ No newline at end of file +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# Temporary files generated by your text editor or operating system +# belong in git's global ignore instead: +# `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` + +# Ignore bundler config. +/.bundle + +# Ignore all environment files. +/.env* + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore storage (uploaded files in development and any SQLite databases). +/storage/* +!/storage/.keep +/tmp/storage/* +!/tmp/storage/ +!/tmp/storage/.keep + +/public/assets + +# Ignore master key for decrypting credentials and more. +/config/master.key diff --git a/.kamal/hooks/docker-setup.sample b/.kamal/hooks/docker-setup.sample new file mode 100755 index 0000000..2fb07d7 --- /dev/null +++ b/.kamal/hooks/docker-setup.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Docker set up on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-app-boot.sample b/.kamal/hooks/post-app-boot.sample new file mode 100755 index 0000000..70f9c4b --- /dev/null +++ b/.kamal/hooks/post-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booted app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/post-deploy.sample b/.kamal/hooks/post-deploy.sample new file mode 100755 index 0000000..75efafc --- /dev/null +++ b/.kamal/hooks/post-deploy.sample @@ -0,0 +1,14 @@ +#!/bin/sh + +# A sample post-deploy hook +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" diff --git a/.kamal/hooks/post-proxy-reboot.sample b/.kamal/hooks/post-proxy-reboot.sample new file mode 100755 index 0000000..1435a67 --- /dev/null +++ b/.kamal/hooks/post-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooted kamal-proxy on $KAMAL_HOSTS" diff --git a/.kamal/hooks/pre-app-boot.sample b/.kamal/hooks/pre-app-boot.sample new file mode 100755 index 0000000..45f7355 --- /dev/null +++ b/.kamal/hooks/pre-app-boot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Booting app version $KAMAL_VERSION on $KAMAL_HOSTS..." diff --git a/.kamal/hooks/pre-build.sample b/.kamal/hooks/pre-build.sample new file mode 100755 index 0000000..f87d811 --- /dev/null +++ b/.kamal/hooks/pre-build.sample @@ -0,0 +1,51 @@ +#!/bin/sh + +# A sample pre-build hook +# +# Checks: +# 1. We have a clean checkout +# 2. A remote is configured +# 3. The branch has been pushed to the remote +# 4. The version we are deploying matches the remote +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +if [ -n "$(git status --porcelain)" ]; then + echo "Git checkout is not clean, aborting..." >&2 + git status --porcelain >&2 + exit 1 +fi + +first_remote=$(git remote) + +if [ -z "$first_remote" ]; then + echo "No git remote set, aborting..." >&2 + exit 1 +fi + +current_branch=$(git branch --show-current) + +if [ -z "$current_branch" ]; then + echo "Not on a git branch, aborting..." >&2 + exit 1 +fi + +remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) + +if [ -z "$remote_head" ]; then + echo "Branch not pushed to remote, aborting..." >&2 + exit 1 +fi + +if [ "$KAMAL_VERSION" != "$remote_head" ]; then + echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 + exit 1 +fi + +exit 0 diff --git a/.kamal/hooks/pre-connect.sample b/.kamal/hooks/pre-connect.sample new file mode 100755 index 0000000..18e61d7 --- /dev/null +++ b/.kamal/hooks/pre-connect.sample @@ -0,0 +1,47 @@ +#!/usr/bin/env ruby + +# A sample pre-connect check +# +# Warms DNS before connecting to hosts in parallel +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) +# KAMAL_RUNTIME + +hosts = ENV["KAMAL_HOSTS"].split(",") +results = nil +max = 3 + +elapsed = Benchmark.realtime do + results = hosts.map do |host| + Thread.new do + tries = 1 + + begin + Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) + rescue SocketError + if tries < max + puts "Retrying DNS warmup: #{host}" + tries += 1 + sleep rand + retry + else + puts "DNS warmup failed: #{host}" + host + end + end + + tries + end + end.map(&:value) +end + +retries = results.sum - hosts.size +nopes = results.count { |r| r == max } + +puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] diff --git a/.kamal/hooks/pre-deploy.sample b/.kamal/hooks/pre-deploy.sample new file mode 100755 index 0000000..1b280c7 --- /dev/null +++ b/.kamal/hooks/pre-deploy.sample @@ -0,0 +1,109 @@ +#!/usr/bin/env ruby + +# A sample pre-deploy hook +# +# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. +# +# Fails unless the combined status is "success" +# +# These environment variables are available: +# KAMAL_RECORDED_AT +# KAMAL_PERFORMER +# KAMAL_VERSION +# KAMAL_HOSTS +# KAMAL_COMMAND +# KAMAL_SUBCOMMAND +# KAMAL_ROLE (if set) +# KAMAL_DESTINATION (if set) + +# Only check the build status for production deployments +if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" + exit 0 +end + +require "bundler/inline" + +# true = install gems so this is fast on repeat invocations +gemfile(true, quiet: true) do + source "https://rubygems.org" + + gem "octokit" + gem "faraday-retry" +end + +MAX_ATTEMPTS = 72 +ATTEMPTS_GAP = 10 + +def exit_with_error(message) + $stderr.puts message + exit 1 +end + +class GithubStatusChecks + attr_reader :remote_url, :git_sha, :github_client, :combined_status + + def initialize + @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") + @git_sha = `git rev-parse HEAD`.strip + @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) + refresh! + end + + def refresh! + @combined_status = github_client.combined_status(remote_url, git_sha) + end + + def state + combined_status[:state] + end + + def first_status_url + first_status = combined_status[:statuses].find { |status| status[:state] == state } + first_status && first_status[:target_url] + end + + def complete_count + combined_status[:statuses].count { |status| status[:state] != "pending"} + end + + def total_count + combined_status[:statuses].count + end + + def current_status + if total_count > 0 + "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." + else + "Build not started..." + end + end +end + + +$stdout.sync = true + +puts "Checking build status..." +attempts = 0 +checks = GithubStatusChecks.new + +begin + loop do + case checks.state + when "success" + puts "Checks passed, see #{checks.first_status_url}" + exit 0 + when "failure" + exit_with_error "Checks failed, see #{checks.first_status_url}" + when "pending" + attempts += 1 + end + + exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS + + puts checks.current_status + sleep(ATTEMPTS_GAP) + checks.refresh! + end +rescue Octokit::NotFound + exit_with_error "Build status could not be found" +end diff --git a/.kamal/hooks/pre-proxy-reboot.sample b/.kamal/hooks/pre-proxy-reboot.sample new file mode 100755 index 0000000..061f805 --- /dev/null +++ b/.kamal/hooks/pre-proxy-reboot.sample @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." diff --git a/.kamal/secrets b/.kamal/secrets new file mode 100644 index 0000000..9a771a3 --- /dev/null +++ b/.kamal/secrets @@ -0,0 +1,17 @@ +# Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, +# and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either +# password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. + +# Example of extracting secrets from 1password (or another compatible pw manager) +# SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) +# KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) +# RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) + +# Use a GITHUB_TOKEN if private repositories are needed for the image +# GITHUB_TOKEN=$(gh config get -h github.com oauth_token) + +# Grab the registry password from ENV +KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD + +# Improve security by using a password manager. Never check config/master.key into git! +RAILS_MASTER_KEY=$(cat config/master.key) diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..f9d86d4 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,8 @@ +# Omakase Ruby styling for Rails +inherit_gem: { rubocop-rails-omakase: rubocop.yml } + +# Overwrite or add rules to create your own house style +# +# # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` +# Layout/SpaceInsideArrayLiteralBrackets: +# Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..be94e6f --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.2.2 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8af8466 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,72 @@ +# syntax=docker/dockerfile:1 +# check=error=true + +# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: +# docker build -t hotwire_native_demo . +# docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name hotwire_native_demo hotwire_native_demo + +# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html + +# Make sure RUBY_VERSION matches the Ruby version in .ruby-version +ARG RUBY_VERSION=3.2.2 +FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base + +# Rails app lives here +WORKDIR /rails + +# Install base packages +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Set production environment +ENV RAILS_ENV="production" \ + BUNDLE_DEPLOYMENT="1" \ + BUNDLE_PATH="/usr/local/bundle" \ + BUNDLE_WITHOUT="development" + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build gems +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential git pkg-config && \ + rm -rf /var/lib/apt/lists /var/cache/apt/archives + +# Install application gems +COPY Gemfile Gemfile.lock ./ +RUN bundle install && \ + rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ + bundle exec bootsnap precompile --gemfile + +# Copy application code +COPY . . + +# Precompile bootsnap code for faster boot times +RUN bundle exec bootsnap precompile app/ lib/ + +# Precompiling assets for production without requiring secret RAILS_MASTER_KEY +RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile + + + + +# Final stage for app image +FROM base + +# Copy built artifacts: gems, application +COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" +COPY --from=build /rails /rails + +# Run and own only the runtime files as a non-root user for security +RUN groupadd --system --gid 1000 rails && \ + useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ + chown -R rails:rails db log storage tmp +USER 1000:1000 + +# Entrypoint prepares the database. +ENTRYPOINT ["/rails/bin/docker-entrypoint"] + +# Start server via Thruster by default, this can be overwritten at runtime +EXPOSE 80 +CMD ["./bin/thrust", "./bin/rails", "server"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..25c0933 --- /dev/null +++ b/Gemfile @@ -0,0 +1,63 @@ +source "https://rubygems.org" + +# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" +gem "rails", "~> 8.0.1" +# The modern asset pipeline for Rails [https://github.com/rails/propshaft] +gem "propshaft" +# Use sqlite3 as the database for Active Record +gem "sqlite3", ">= 2.1" +# Use the Puma web server [https://github.com/puma/puma] +gem "puma", ">= 5.0" +# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] +gem "importmap-rails" +# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] +gem "turbo-rails" +# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] +gem "stimulus-rails" +# Build JSON APIs with ease [https://github.com/rails/jbuilder] +gem "jbuilder" + +# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] +# gem "bcrypt", "~> 3.1.7" + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem "tzinfo-data", platforms: %i[ windows jruby ] + +# Use the database-backed adapters for Rails.cache, Active Job, and Action Cable +gem "solid_cache" +gem "solid_queue" +gem "solid_cable" + +# Reduces boot times through caching; required in config/boot.rb +gem "bootsnap", require: false + +# Deploy this application anywhere as a Docker container [https://kamal-deploy.org] +gem "kamal", require: false + +# Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] +gem "thruster", require: false + +# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] +# gem "image_processing", "~> 1.2" + +group :development, :test do + # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem + gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" + + # Static analysis for security vulnerabilities [https://brakemanscanner.org/] + gem "brakeman", require: false + + # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] + gem "rubocop-rails-omakase", require: false +end + +group :development do + # Use console on exceptions pages [https://github.com/rails/web-console] + gem "web-console" +end + +group :test do + # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] + gem "capybara" + gem "selenium-webdriver" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..78d42b7 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,368 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (8.0.1) + actionpack (= 8.0.1) + activesupport (= 8.0.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (8.0.1) + actionpack (= 8.0.1) + activejob (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) + mail (>= 2.8.0) + actionmailer (8.0.1) + actionpack (= 8.0.1) + actionview (= 8.0.1) + activejob (= 8.0.1) + activesupport (= 8.0.1) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (8.0.1) + actionview (= 8.0.1) + activesupport (= 8.0.1) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (8.0.1) + actionpack (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (8.0.1) + activesupport (= 8.0.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (8.0.1) + activesupport (= 8.0.1) + globalid (>= 0.3.6) + activemodel (8.0.1) + activesupport (= 8.0.1) + activerecord (8.0.1) + activemodel (= 8.0.1) + activesupport (= 8.0.1) + timeout (>= 0.4.0) + activestorage (8.0.1) + actionpack (= 8.0.1) + activejob (= 8.0.1) + activerecord (= 8.0.1) + activesupport (= 8.0.1) + marcel (~> 1.0) + activesupport (8.0.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.2) + base64 (0.2.0) + bcrypt_pbkdf (1.1.1) + bcrypt_pbkdf (1.1.1-arm64-darwin) + benchmark (0.4.0) + bigdecimal (3.1.9) + bindex (0.8.1) + bootsnap (1.18.4) + msgpack (~> 1.2) + brakeman (7.0.0) + racc + builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + concurrent-ruby (1.3.5) + connection_pool (2.5.0) + crass (1.0.6) + date (3.4.1) + debug (1.10.0) + irb (~> 1.10) + reline (>= 0.3.8) + dotenv (3.1.7) + drb (2.2.1) + ed25519 (1.3.0) + erubi (1.13.1) + et-orbi (1.2.11) + tzinfo + fugit (1.11.1) + et-orbi (~> 1, >= 1.2.11) + raabro (~> 1.4) + globalid (1.2.1) + activesupport (>= 6.1) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + importmap-rails (2.1.0) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.0) + irb (1.15.1) + pp (>= 0.6.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + jbuilder (2.13.0) + actionview (>= 5.0.0) + activesupport (>= 5.0.0) + json (2.9.1) + kamal (2.5.2) + activesupport (>= 7.0) + base64 (~> 0.2) + bcrypt_pbkdf (~> 1.0) + concurrent-ruby (~> 1.2) + dotenv (~> 3.1) + ed25519 (~> 1.2) + net-ssh (~> 7.3) + sshkit (>= 1.23.0, < 2.0) + thor (~> 1.3) + zeitwerk (>= 2.6.18, < 3.0) + language_server-protocol (3.17.0.4) + logger (1.6.5) + loofah (2.24.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + matrix (0.4.2) + mini_mime (1.1.5) + minitest (5.25.4) + msgpack (1.8.0) + net-imap (0.5.6) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-scp (4.1.0) + net-ssh (>= 2.6.5, < 8.0.0) + net-sftp (4.0.0) + net-ssh (>= 5.0.0, < 8.0.0) + net-smtp (0.5.1) + net-protocol + net-ssh (7.3.0) + nio4r (2.7.4) + nokogiri (1.18.2-aarch64-linux-gnu) + racc (~> 1.4) + nokogiri (1.18.2-arm64-darwin) + racc (~> 1.4) + nokogiri (1.18.2-x86_64-linux-gnu) + racc (~> 1.4) + ostruct (0.6.1) + parallel (1.26.3) + parser (3.3.7.1) + ast (~> 2.4.1) + racc + pp (0.6.2) + prettyprint + prettyprint (0.2.0) + propshaft (1.1.0) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack + railties (>= 7.0.0) + psych (5.2.3) + date + stringio + public_suffix (6.0.1) + puma (6.6.0) + nio4r (~> 2.0) + raabro (1.4.0) + racc (1.8.1) + rack (3.1.9) + rack-session (2.1.0) + base64 (>= 0.1.0) + rack (>= 3.0.0) + rack-test (2.2.0) + rack (>= 1.3) + rackup (2.2.1) + rack (>= 3) + rails (8.0.1) + actioncable (= 8.0.1) + actionmailbox (= 8.0.1) + actionmailer (= 8.0.1) + actionpack (= 8.0.1) + actiontext (= 8.0.1) + actionview (= 8.0.1) + activejob (= 8.0.1) + activemodel (= 8.0.1) + activerecord (= 8.0.1) + activestorage (= 8.0.1) + activesupport (= 8.0.1) + bundler (>= 1.15.0) + railties (= 8.0.1) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.2) + loofah (~> 2.21) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) + railties (8.0.1) + actionpack (= 8.0.1) + activesupport (= 8.0.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.2.1) + rdoc (6.12.0) + psych (>= 4.0.0) + regexp_parser (2.10.0) + reline (0.6.0) + io-console (~> 0.5) + rexml (3.4.0) + rubocop (1.71.2) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.38.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.38.0) + parser (>= 3.3.1.0) + rubocop-minitest (0.36.0) + rubocop (>= 1.61, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-performance (1.23.1) + rubocop (>= 1.48.1, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails (2.29.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.52.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) + rubocop-rails-omakase (1.0.0) + rubocop + rubocop-minitest + rubocop-performance + rubocop-rails + ruby-progressbar (1.13.0) + rubyzip (2.4.1) + securerandom (0.4.1) + selenium-webdriver (4.28.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + solid_cable (3.0.7) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.7) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.1.3) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11.0) + railties (>= 7.1) + thor (~> 1.3.1) + sqlite3 (2.5.0-aarch64-linux-gnu) + sqlite3 (2.5.0-arm64-darwin) + sqlite3 (2.5.0-x86_64-linux-gnu) + sshkit (1.23.2) + base64 + net-scp (>= 1.1.2) + net-sftp (>= 2.1.2) + net-ssh (>= 2.8.0) + ostruct + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.1.2) + thor (1.3.2) + thruster (0.1.10-aarch64-linux) + thruster (0.1.10-arm64-darwin) + thruster (0.1.10-x86_64-linux) + timeout (0.4.3) + turbo-rails (2.0.11) + actionpack (>= 6.0.0) + railties (>= 6.0.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) + uri (1.0.2) + useragent (0.16.11) + web-console (4.2.1) + actionview (>= 6.0.0) + activemodel (>= 6.0.0) + bindex (>= 0.4.0) + railties (>= 6.0.0) + websocket (1.2.11) + websocket-driver (0.7.7) + base64 + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.7.1) + +PLATFORMS + aarch64-linux + arm64-darwin-22 + arm64-darwin-23 + x86_64-linux + +DEPENDENCIES + bootsnap + brakeman + capybara + debug + importmap-rails + jbuilder + kamal + propshaft + puma (>= 5.0) + rails (~> 8.0.1) + rubocop-rails-omakase + selenium-webdriver + solid_cable + solid_cache + solid_queue + sqlite3 (>= 2.1) + stimulus-rails + thruster + turbo-rails + tzinfo-data + web-console + +BUNDLED WITH + 2.4.22 diff --git a/README.md b/README.md index 10e93c3..7db80e4 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,24 @@ -# Hotwire Native Demo +# README -A small web app to demonstrate how to use Hotwire with the Hotwire Native frameworks. The demo app is available at [https://hotwire-native-demo.dev](https://hotwire-native-demo.dev) +This README would normally document whatever steps are necessary to get the +application up and running. -## Running Locally +Things you may want to cover: -Clone the repo, and then: +* Ruby version -``` -$ npm install -$ npx nodemon -``` +* System dependencies -The server is running on [`localhost:45678`](http://localhost:45678). You can open that url in the browser and ensure the native app is using the same url. +* Configuration + +* Database creation + +* Database initialization + +* How to run the test suite + +* Services (job queues, cache servers, search engines, etc.) + +* Deployment instructions + +* ... diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/images/sprite-sheet.svg b/app/assets/images/sprite-sheet.svg new file mode 100644 index 0000000..80b245a --- /dev/null +++ b/app/assets/images/sprite-sheet.svg @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000..64a6565 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,42 @@ +/* + * This is a manifest file that'll be compiled into application.css. + * + * With Propshaft, assets are served efficiently without preprocessing steps. You can still include + * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard + * cascading order, meaning styles declared later in the document or manifest will override earlier ones, + * depending on specificity. + * + * Consider organizing styles into separate files for maintainability. + */ + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + /* Disable smooth scroll from Bootstrap. */ + scroll-behavior: auto !important; + font-size: 100%; +} + +body { + margin: 0; + background: var(--color-body); + color: var(--color-on-body); + font-family: var(--font-family); + font-size: var(--text-body-size); + line-height: var(--text-line-height); +} + +code { + color: color-mix(in srgb, var(--color-accent), var(--color-on-body) 20%); + word-wrap: break-word; +} + +a:focus-visible { + border-radius: 0.5rem; + outline: 2px solid var(--color-on-body); + outline-offset: 1px; +} diff --git a/app/assets/stylesheets/containers.css b/app/assets/stylesheets/containers.css new file mode 100644 index 0000000..cc9bb38 --- /dev/null +++ b/app/assets/stylesheets/containers.css @@ -0,0 +1,188 @@ +/* + * Top Level Container + */ + +.top-level-container { + padding-inline: var(--top-level-container-padding); +} + +/* + * Flex Container + */ + +.flex-container, +.flush-flex-container { + display: flex; + flex-wrap: wrap; + align-items: stretch; + align-content: start; +} + +.flex-container { + margin-inline: calc(var(--space-gutter) * -0.5); +} + +.flex-column, +.flush-flex-column { + flex: 1 0 0%; + max-inline-size: 100%; +} + +.flex-column { + padding-inline: calc(var(--space-gutter) * 0.5); +} + +/* + * Dialog + */ + +.dialog { + padding: unset; + border: unset; + border-radius: 1rem; + background: var(--color-body); + box-shadow: + 0 0.2rem 0.4rem 0 rgba(0,0,0, 0.1), + 0 1rem 4rem 0 rgba(0,0,0, 0.4); + + &::backdrop { + background: rgba(0, 0, 0, 0.5); + } +} + +.dialog__content { + padding: var(--space-large); + + @media (min-width: 35rem) { + padding: var(--space-x-large); + } +} + +.dialog__close-button { + position: absolute; + inset-inline-end: 1rem; + inset-block-start: 1rem; +} + +/* + * Alert + */ + +.alert { + padding: var(--space-large); + border-radius: 1rem; + background: var(--color-surface); + color: var(--color-on-surface); +} + +.alert__header { + margin: 0 0 var(--space-medium) 0; +} + +.alert--warning { + background: color-mix(in srgb, var(--color-warning), var(--color-body) 90%); + color: var(--color-warning); + + .alert__header { + color: color-mix(in srgb, var(--color-warning), var(--color-on-body) 20%); + } +} + +.alert--success { + background: color-mix(in srgb, var(--color-success), var(--color-body) 90%); + color: var(--color-success); + + .alert__header { + color: color-mix(in srgb, var(--color-success), var(--color-on-body) 20%); + } +} + +/* + * Formatted List + */ + +.formatted-list { + @media (min-width: 60rem) { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: var(--space-x-large); + } +} + +.formatted-list__item { + display: flex; + align-items: center; + gap: var(--space-x-large); + padding-block: var(--space-large); + border-block-end: 1px solid var(--color-outline); + color: inherit; + text-decoration: inherit; + + @media (max-width: calc(60rem - 1px)) { + &:last-child { + border-bottom: unset; + } + } +} + +@media (hover: hover) and (pointer: fine) { + a.formatted-list__item:hover { + background: var(--color-surface); + } +} + +.formatted-list--top-level { + margin-inline: calc(var(--top-level-container-padding) * -1); + + .formatted-list__item { + padding-inline: var(--top-level-container-padding); + } + + @media (min-width: 35rem) { + margin-inline: unset; + + .formatted-list__item { + padding-inline: 1.5rem; + } + } +} + +/* + * Tabs + */ + +.tabs { + display: flex; + border-bottom: 1px solid var(--color-outline); +} + +.tabs__item { + display: block; + + & + & { + margin-inline-start: -1px; + } + + a { + display: block; + padding: 0.5rem 1rem; + border: 1px solid var(--color-outline); + border-bottom-color: transparent; + border-radius: 0.5rem 0.5rem 0 0; + margin-bottom: -1px; + color: inherit; + text-decoration: unset; + } + + a.active { + border-bottom-color: var(--color-body); + } + + &:first-child a { + border-radius: 1rem 0.5rem 0 0; + } + + &:last-child a { + border-radius: 0.5rem 1rem 0 0; + } +} diff --git a/app/assets/stylesheets/form.css b/app/assets/stylesheets/form.css new file mode 100644 index 0000000..5008923 --- /dev/null +++ b/app/assets/stylesheets/form.css @@ -0,0 +1,162 @@ +/* + * Forms + */ + +label { + display: inline-block; + vertical-align: baseline; +} + +input[type="date"], +input[type="datetime-local"], +input[type="email"], +input[type="month"], +input[type="number"], +input[type="password"], +input[type="search"], +input[type="tel"], +input[type="text"], +input[type="time"], +input[type="url"], +input[type="week"], +input[type="file"], +meter, +progress, +select, +textarea { + overflow: hidden; + outline: unset; + box-shadow: unset; + color: var(--color-on-body); + font-family: var(--font-family); + font-size: var(--text-body-size); + font-style: normal; + font-weight: normal; + letter-spacing: unset; + line-height: var(--text-line-height); + text-decoration: unset; + text-transform: unset; + + &:disabled { + cursor: default; + background: color(from var(--color-subtle) srgb r g b / 0.2); + opacity: var(--form-disabled-control-opacity); + filter: saturate(50%); + } + + &:focus-visible { + outline: 2px solid var(--color-on-body); + outline-offset: 1px; + } +} + +input[type="date"], +input[type="datetime-local"], +input[type="email"], +input[type="month"], +input[type="number"], +input[type="password"], +input[type="search"], +input[type="tel"], +input[type="text"], +input[type="time"], +input[type="url"], +input[type="week"], +input[type="file"], +textarea { + padding: 0.5rem; + border: 1px solid var(--color-subtle); + border-radius: 0.5rem; +} + +input[type="file"] { + padding: 0; + + &::file-selector-button { + padding: 0.5rem; + border: unset; + border-right: 1px solid var(--color-subtle); + border-radius: unset; + outline: unset; + box-shadow: unset; + margin-inline-end: var(--space-medium); + background: var(--color-surface); + color: var(--color-on-surface); + font-size: unset; + } +} + +.form-label { + display: block; + margin-bottom: var(--space-medium); + font-weight: 500; +} + +.form-control { + display: block; + max-inline-size: 100%; +} + +/* + * Buttons + */ + +.button, +.unstyled-button { + cursor: pointer; + pointer-events: auto; + overflow: hidden; + display: inline-flex; + outline: unset; + box-shadow: unset; + color: var(--color-on-body); + font-family: var(--font-family); + font-size: var(--text-body-size); + font-style: normal; + font-weight: normal; + letter-spacing: unset; + line-height: var(--text-line-height); + text-align: center; + text-decoration: unset; + text-overflow: ellipsis; + text-transform: unset; + + &:disabled { + cursor: default; + } + + &:focus-visible { + outline: 2px solid var(--color-on-body); + outline-offset: 1px; + } + + &:active:not(:disabled) { + transform: scale(0.96); + } +} + +.button { + align-items: center; + justify-content: center; + padding: 0.5rem 1rem; + border: 1px solid var(--color-accent); + border-radius: 0.5rem; + background: var(--color-accent); + color: var(--color-on-accent); + + &:disabled { + border-color: color-mix(in srgb, var(--color-accent) 20%, var(--color-subtle)); + background-color: color-mix(in srgb, var(--color-accent) 20%, var(--color-subtle)); + } +} + +.unstyled-button { + padding: unset; + border: unset; + border-radius: unset; + background: unset; + + &:disabled { + opacity: 0.5; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/header.css b/app/assets/stylesheets/header.css new file mode 100644 index 0000000..02d97ff --- /dev/null +++ b/app/assets/stylesheets/header.css @@ -0,0 +1,49 @@ +/* + * Header + */ + +.header { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: start; + background: var(--color-surface); + color: var(--color-on-surface); + + @media (min-width: 35rem) { + flex-direction: row; + justify-content: space-between; + align-items: stretch; + } +} + +.header__brand { + svg { + display: block; + inline-size: 10em; + block-size: 4em; + } +} + +/* + * Main navigation + */ + +.main-navigation { + display: flex; + justify-content: space-between; + align-items: stretch; + gap: 1em; + text-align: center; + + @media (min-width: 45rem) { + gap: 2em; + } +} + +.main-navigation__link { + display: flex; + block-size: 100%; + align-items: center; + padding-block: 1rem; +} diff --git a/app/assets/stylesheets/native.css b/app/assets/stylesheets/native.css new file mode 100644 index 0000000..001ad7e --- /dev/null +++ b/app/assets/stylesheets/native.css @@ -0,0 +1,17 @@ +/* Hide elements in Hotwire Native apps. */ +.hide\@native { + display: none !important; +} + +/* Hide the submit button when the "form" component is registered. */ +[data-bridge-components~="form"] +[data-controller~="bridge--form"] +[type="submit"] { + display: none; +} + +/* Hide the overflow button when the "overflow-menu" component is registered. */ +[data-bridge-components~="overflow-menu"] +[data-controller~="bridge--overflow-menu"] { + display: none; +} diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css new file mode 100644 index 0000000..3e8d1e4 --- /dev/null +++ b/app/assets/stylesheets/utilities.css @@ -0,0 +1,214 @@ +/* + * Links + */ + +.unstyled-link, +.decorated-link { + color: inherit; + text-decoration: inherit; +} + +@media (hover: hover) and (pointer: fine) { + .decorated-link:hover { + background: linear-gradient(to top, currentColor 2px, transparent 0) bottom no-repeat; + color: var(--color-accent); + animation: 0.2s decorated-link both; + } +} + +@keyframes decorated-link { + from { background-size: 0 100%; } + to { background-size: 100% 100%; } +} + +/* + * Lists + */ + +.unstyled-list { + list-style: none; + padding: 0; + margin: 0; + + dt, dd { + display: inline-block; + margin: 0; + } +} + +/* + * Icons + */ + +.medium-icon, +.small-icon { + display: inline-block; +} + +.medium-icon { + inline-size: 1.5rem; + block-size: 1.5rem; +} + +.small-icon { + inline-size: 0.75rem; + block-size: 0.75rem; +} + +/* + * Colors + */ + +.color-accent { color: var(--color-accent); } +.color-on-body-muted { color: var(--color-on-body-muted); } +.color-subtle { color: var(--color-subtle); } + +/* + * Text + */ + +.text-body { + font-size: var(--text-body-size); + line-height: var(--text-line-height); +} + +.text-headline { + font-size: var(--text-headline-size); + font-weight: 500; + line-height: var(--text-line-height); +} + +.text-title { + font-size: var(--text-title-size); + font-weight: 600; + line-height: 1.2; +} + +.text-large-title { + font-size: var(--text-large-title-size); + font-weight: 700; + line-height: 1; +} + +/* + * Size + */ + +.inline-100 { inline-size: 100% } +.block-100 { block-size: 100% } + +.flex-fill { + flex: 1 0 0%; + max-inline-size: 100%; +} + +.flex-100 { + flex: 0 0 100%; + max-inline-size: 100%; +} + +@media (min-width: 45rem) { + .flex-6\@l { + flex: 0 0 calc(100% / 12 * 6); + max-inline-size: calc(100% / 12 * 6); + } +} + +@media (min-width: 60rem) { + .flex-5\@xl { + flex: 0 0 calc(100% / 12 * 5); + max-inline-size: calc(100% / 12 * 5); + } +} + +/* + * Alignments + */ + +.align-self-end { + align-self: end; +} + +/* + * Spaces + */ + +.gap { gap: var(--space-medium); } +.row-gap { row-gap: var(--space-medium); } +.column-gap { column-gap: var(--space-medium); } + +.gap-gutter { gap: var(--space-gutter); } +.row-gap-gutter { row-gap: var(--space-gutter); } +.column-gap-gutter { column-gap: var(--space-gutter); } + +/* + * Margins + */ + +.margin-i { margin-inline: var(--space-medium); } +.margin-i-l { margin-inline: var(--space-large); } +.margin-i-xl { margin-inline: var(--space-x-large); } +.margin-i-xxl { margin-inline: var(--space-xx-large); } + +.margin-is { margin-inline-start: var(--space-medium); } +.margin-is-l { margin-inline-start: var(--space-large); } +.margin-is-xl { margin-inline-start: var(--space-x-large); } +.margin-is-xxl { margin-inline-start: var(--space-xx-large); } + +.margin-ie { margin-inline-end: var(--space-medium); } +.margin-ie-l { margin-inline-end: var(--space-large); } +.margin-ie-xl { margin-inline-end: var(--space-x-large); } +.margin-ie-xxl { margin-inline-end: var(--space-xx-large); } + +.margin-b { margin-block: var(--space-medium); } +.margin-b-l { margin-block: var(--space-large); } +.margin-b-xl { margin-block: var(--space-x-large); } +.margin-b-xxl { margin-block: var(--space-xx-large); } + +.margin-bs { margin-block-start: var(--space-medium); } +.margin-bs-l { margin-block-start: var(--space-large); } +.margin-bs-xl { margin-block-start: var(--space-x-large); } +.margin-bs-xxl { margin-block-start: var(--space-xx-large); } + +.margin-be { margin-block-end: var(--space-medium); } +.margin-be-l { margin-block-end: var(--space-large); } +.margin-be-xl { margin-block-end: var(--space-x-large); } +.margin-be-xxl { margin-block-end: var(--space-xx-large); } + +.no-margin { margin: 0; } + +/* + * Paddings + */ + +.padding-i { padding-inline: var(--space-medium); } +.padding-i-l { padding-inline: var(--space-large); } +.padding-i-xl { padding-inline: var(--space-x-large); } +.padding-i-xxl { padding-inline: var(--space-xx-large); } + +.padding-is { padding-inline-start: var(--space-medium); } +.padding-is-l { padding-inline-start: var(--space-large); } +.padding-is-xl { padding-inline-start: var(--space-x-large); } +.padding-is-xxl { padding-inline-start: var(--space-xx-large); } + +.padding-ie { padding-inline-end: var(--space-medium); } +.padding-ie-l { padding-inline-end: var(--space-large); } +.padding-ie-xl { padding-inline-end: var(--space-x-large); } +.padding-ie-xxl { padding-inline-end: var(--space-xx-large); } + +.padding-b { padding-block: var(--space-medium); } +.padding-b-l { padding-block: var(--space-large); } +.padding-b-xl { padding-block: var(--space-x-large); } +.padding-b-xxl { padding-block: var(--space-xx-large); } + +.padding-bs { padding-block-start: var(--space-medium); } +.padding-bs-l { padding-block-start: var(--space-large); } +.padding-bs-xl { padding-block-start: var(--space-x-large); } +.padding-bs-xxl { padding-block-start: var(--space-xx-large); } + +.padding-be { padding-block-end: var(--space-medium); } +.padding-be-l { padding-block-end: var(--space-large); } +.padding-be-xl { padding-block-end: var(--space-x-large); } +.padding-be-xxl { padding-block-end: var(--space-xx-large); } + +.no-padding { padding: 0; } diff --git a/app/assets/stylesheets/variables.css b/app/assets/stylesheets/variables.css new file mode 100644 index 0000000..61d49d8 --- /dev/null +++ b/app/assets/stylesheets/variables.css @@ -0,0 +1,74 @@ +:root { + /* + * Colors + */ + + --color-accent: rgba(193, 139, 244, 1); + --color-on-accent: rgba(255, 255, 255, 1); + + --color-body: rgba(255, 255, 255, 1); + --color-on-body: rgba(0, 0, 0, 1); + --color-on-body-muted: rgba(0, 20, 40, 0.6); + + --color-surface: rgba(246, 247, 248, 1); + --color-on-surface: rgba(0, 0, 0, 1); + + --color-subtle: rgba(172, 176, 180, 1); + --color-outline: rgba(222, 226, 230, 1); + + --color-warning: rgba(196, 78, 57, 1); + --color-success: rgba(84, 163, 104, 1); + + /* + * Fonts + */ + + --font-family: -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + + /* + * Text + */ + + --text-body-size: 1rem; + --text-headline-size: 1rem; + --text-title-size: 1.4rem; + --text-large-title-size: 2rem; + + --text-line-height: 1.5; + + /* + * Breakpoints + */ + + --breakpoint-medium: 35rem; /* 560 */ + --breakpoint-large: 45rem; /* 720 */ + --breakpoint-x-large: 60rem; /* 960 */ + --breakpoint-xx-large: 80rem; /* 1280 */ + + /* + * Spaces + */ + + --space-medium: 0.5rem; + --space-large: calc(var(--space-medium) * 2); + --space-x-large: calc(var(--space-large) * 2); + --space-xx-large: calc(var(--space-large) * 4); + + --space-gutter: 1.5rem; + + /* + * Top Level Container + */ + + --top-level-container-padding: max(1rem, 4%); + + @media (min-width: 80rem) { + --top-level-container-padding: max(4%, calc(50% - 40rem)); + } + + /* + * Forms + */ + + --form-disabled-control-opacity: 0.6; +} \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..0d95db2 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,4 @@ +class ApplicationController < ActionController::Base + # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. + allow_browser versions: :modern +end diff --git a/app/controllers/bugs_controller.rb b/app/controllers/bugs_controller.rb new file mode 100644 index 0000000..aa044e9 --- /dev/null +++ b/app/controllers/bugs_controller.rb @@ -0,0 +1,8 @@ +class BugsController < ApplicationController + def index + end + + def tabs + @tab = params[:tab] || "first" + end +end diff --git a/app/controllers/components_controller.rb b/app/controllers/components_controller.rb new file mode 100644 index 0000000..dd90e3e --- /dev/null +++ b/app/controllers/components_controller.rb @@ -0,0 +1,25 @@ +class ComponentsController < ApplicationController + def index + end + + def new + end + + def create + redirect_to component_path( + first_name: params[:first_name], + last_name: params[:last_name] + ) + end + + def show + @first_name = params[:first_name] + @last_name = params[:last_name] + end + + def menu + end + + def overflow + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/configurations_controller.rb b/app/controllers/configurations_controller.rb new file mode 100644 index 0000000..c5033f9 --- /dev/null +++ b/app/controllers/configurations_controller.rb @@ -0,0 +1,41 @@ +class ConfigurationsController < ApplicationController + def ios_v1 + render json: { + settings: { + enable_feature_x: true + }, + rules: [ + { + patterns: [ + "/new$", + "/edit$", + "/modal" + ], + properties: { + context: "modal", + pull_to_refresh_enabled: false + }, + comment: "Present forms and custom modal path as modals." + }, + { + patterns: [ + "/numbers$" + ], + properties: { + view_controller: "numbers" + }, + comment: "Intercept with a native view." + }, + { + patterns: [ + "^/$" + ], + properties: { + presentation: "clear_all" + }, + comment: "Reset navigation stacks when visiting root page." + } + ] + } + end +end diff --git a/app/controllers/dashboards_controller.rb b/app/controllers/dashboards_controller.rb new file mode 100644 index 0000000..aed6610 --- /dev/null +++ b/app/controllers/dashboards_controller.rb @@ -0,0 +1,4 @@ +class DashboardsController < ApplicationController + def show + end +end diff --git a/app/controllers/modals_controller.rb b/app/controllers/modals_controller.rb new file mode 100644 index 0000000..abae90d --- /dev/null +++ b/app/controllers/modals_controller.rb @@ -0,0 +1,14 @@ +class ModalsController < ApplicationController + def new + end + + def show + end + + def redirect + redirect_to turbo_refresh_historical_location_path + end + + def replace + end +end diff --git a/app/controllers/navigations_controller.rb b/app/controllers/navigations_controller.rb new file mode 100644 index 0000000..358ba97 --- /dev/null +++ b/app/controllers/navigations_controller.rb @@ -0,0 +1,21 @@ +class NavigationsController < ApplicationController + def show + end + + def redirect + redirect_to redirected_navigation_path + end + + def redirected + end + + def replace + end + + def slow + sleep 1.5 + end + + def second + end +end diff --git a/app/controllers/numbers_controller.rb b/app/controllers/numbers_controller.rb new file mode 100644 index 0000000..e1256f6 --- /dev/null +++ b/app/controllers/numbers_controller.rb @@ -0,0 +1,8 @@ +class NumbersController < ApplicationController + def index + end + + def show + @number = params[:id] + end +end diff --git a/app/controllers/resources_controller.rb b/app/controllers/resources_controller.rb new file mode 100644 index 0000000..d76b22d --- /dev/null +++ b/app/controllers/resources_controller.rb @@ -0,0 +1,40 @@ +class ResourcesController < ApplicationController + def index + end + + def new + @resource = Resource.new + end + + def create + @resource = Resource.new(resource_params) + if @resource.valid? + redirect_to resource_path( + first_name: @resource.first_name, + last_name: @resource.last_name + ), notice: "Form submitted successfully." + else + render :new, status: :unprocessable_entity + end + end + + def show + @first_name = params[:first_name] + @last_name = params[:last_name] + end + + def long + end + + def scroll + end + + def upload + end + + private + + def resource_params + params.require(:resource).permit(:first_name, :last_name) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..cdf25f3 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,12 @@ +module ApplicationHelper + def sprite_tag(name, **options) + content_tag(:svg, **options) do + "".html_safe + end + end + + def icon_tag(name, size: :medium, **options) + class_name = ["#{size}-icon", options.delete(:class)].compact.join(" ") + sprite_tag(name, **options.merge(class: class_name, aria: { hidden: "true" })) + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js new file mode 100644 index 0000000..0d7b494 --- /dev/null +++ b/app/javascript/application.js @@ -0,0 +1,3 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import "@hotwired/turbo-rails" +import "controllers" diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js new file mode 100644 index 0000000..e5fa674 --- /dev/null +++ b/app/javascript/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/public/javascript/controllers/bridge/form_controller.js b/app/javascript/controllers/bridge/form_controller.js similarity index 100% rename from public/javascript/controllers/bridge/form_controller.js rename to app/javascript/controllers/bridge/form_controller.js diff --git a/public/javascript/controllers/bridge/menu_controller.js b/app/javascript/controllers/bridge/menu_controller.js similarity index 96% rename from public/javascript/controllers/bridge/menu_controller.js rename to app/javascript/controllers/bridge/menu_controller.js index e315ece..bdb8361 100644 --- a/public/javascript/controllers/bridge/menu_controller.js +++ b/app/javascript/controllers/bridge/menu_controller.js @@ -3,7 +3,7 @@ import { BridgeElement } from "@hotwired/hotwire-native-bridge" export default class extends BridgeComponent { static component = "menu" - static targets = [ "title", "item" ] + static targets = ["title", "item"] show(event) { if (this.enabled) { diff --git a/public/javascript/controllers/bridge/overflow_menu_controller.js b/app/javascript/controllers/bridge/overflow_menu_controller.js similarity index 88% rename from public/javascript/controllers/bridge/overflow_menu_controller.js rename to app/javascript/controllers/bridge/overflow_menu_controller.js index 1931909..d143c4c 100644 --- a/public/javascript/controllers/bridge/overflow_menu_controller.js +++ b/app/javascript/controllers/bridge/overflow_menu_controller.js @@ -11,7 +11,7 @@ export default class extends BridgeComponent { notifyBridgeOfConnect() { const label = this.bridgeElement.title - this.send("connect", { label }, () => { + this.send("connect", {label}, () => { this.bridgeElement.click() }) } diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js new file mode 100644 index 0000000..1156bf8 --- /dev/null +++ b/app/javascript/controllers/index.js @@ -0,0 +1,4 @@ +// Import and register all your controllers from the importmap via controllers/**/*_controller +import { application } from "controllers/application" +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) diff --git a/public/javascript/controllers/menu_controller.js b/app/javascript/controllers/menu_controller.js similarity index 58% rename from public/javascript/controllers/menu_controller.js rename to app/javascript/controllers/menu_controller.js index 11520b3..8e60c71 100644 --- a/public/javascript/controllers/menu_controller.js +++ b/app/javascript/controllers/menu_controller.js @@ -1,18 +1,18 @@ import { Controller } from "@hotwired/stimulus" export default class extends Controller { - static targets = [ "dialog", "item", "result" ] + static targets = ["dialog", "dialogContent", "item", "result"] show() { - this.dialogTarget.style.display = "block" + this.dialogTarget.showModal() } hide() { - this.dialogTarget.style.display = "none" + this.dialogTarget.close() } hideOnClickOutside({ target }) { - if (this.dialogTarget == target) { + if (this.dialogTarget.contains(target) && !this.dialogContentTarget.contains(target)) { this.hide() } } diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..3c34c81 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "from@example.com" + layout "mailer" +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..b63caeb --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/resource.rb b/app/models/resource.rb new file mode 100644 index 0000000..7f3fcf1 --- /dev/null +++ b/app/models/resource.rb @@ -0,0 +1,7 @@ +class Resource + include ActiveModel::Model + + attr_accessor :first_name, :last_name + + validates :first_name, :last_name, presence: true +end diff --git a/app/views/bugs/index.html.erb b/app/views/bugs/index.html.erb new file mode 100644 index 0000000..e666af4 --- /dev/null +++ b/app/views/bugs/index.html.erb @@ -0,0 +1,14 @@ +<% content_for :title, "Bugs & Fixes" %> + +
+

Bugs & Fixes

+

An area to reproduce outstanding bugs and ensure no regressions on existing fixes reappear.

+ +
+ <%= render "navigations/item", + path: tabs_bugs_path, + icon: "cloud-upload-bold", + name: "Turbo Action Replace", + description: "Web-based tabs that replace screens." %> +
+
diff --git a/app/views/bugs/tabs.html.erb b/app/views/bugs/tabs.html.erb new file mode 100644 index 0000000..b1f3a15 --- /dev/null +++ b/app/views/bugs/tabs.html.erb @@ -0,0 +1,23 @@ +<% content_for :title, "Turbo Action Replace" %> + +
+

Turbo Action Replace

+

Each of these tabs uses data-turbo-action="replace" to present a different tab. The URL path for each is the same, only the query param changes.

+

This reproduces <%= link_to "GitHub issue #53", "https://github.com/hotwired/hotwire-native-ios/issues/53" %>.

+
+ + + +
+

Content for the <%= @tab %> tab.

+
diff --git a/app/views/components/index.html.erb b/app/views/components/index.html.erb new file mode 100644 index 0000000..fda9761 --- /dev/null +++ b/app/views/components/index.html.erb @@ -0,0 +1,32 @@ +<% content_for :title, "Bridge Components" %> + +
+

Bridge Components

+

Hotwire Native abstracts the integration with its corresponding web bridge (formerly Strada), making it even faster to get started.

+ +
+ <%= render "navigations/item", + path: bridge_components_url, + icon: "external-link-bold", + name: "Documentation", + description: "Learn about bridge components." %> + + <%= render "navigations/item", + path: new_component_path, + icon: "text-input-bold", + name: "Form example", + description: "Submit a form with a native button." %> + + <%= render "navigations/item", + path: menu_components_path, + icon: "menu-bold", + name: "Menu example", + description: "Display a native bottom sheet." %> + + <%= render "navigations/item", + path: overflow_components_path, + icon: "overflow-bold", + name: "Overflow menu example", + description: "Display a native 3-dot button." %> +
+
diff --git a/app/views/components/menu.html.erb b/app/views/components/menu.html.erb new file mode 100644 index 0000000..8c56376 --- /dev/null +++ b/app/views/components/menu.html.erb @@ -0,0 +1,45 @@ +<% content_for :title, "Menu Component" %> + +
+

Menu Component

+

This screen contains a menu associated with a Bridge menu component. The page contains a button below to open a web-based menu that contains several options to choose from.

+

Since the Hotwire Native demo app supports the menu component, the web menu is hidden and replaced with a natively displayed menu. Tapping on a native menu option replies back to the web menu component with the selectedIndex of the selected option.

+

Displaying the menu in a native bottom sheet is a typical convention in mobile apps. It blocks touch input for all controls on the screen and allows platform gestures to dismiss the sheet.

+ +
+ + +
+ +
+ + +
+

Select an option

+ + + + +
+
+ +
+
+
+
diff --git a/app/views/components/new.html.erb b/app/views/components/new.html.erb new file mode 100644 index 0000000..d5106e3 --- /dev/null +++ b/app/views/components/new.html.erb @@ -0,0 +1,24 @@ +<% content_for :title, "Form Component" %> + +
+

Form Component

+

This screen contains a form associated with a Bridge form component. It contains a web submit button that submits the form and redirects to a success page after a short delay.

+

Since the Hotwire Native demo app supports the form component, the web submit button is hidden and is replaced with a native button in the top-right native app bar.

+

Displaying the submit button in the top-right of the screen is a typical convention in mobile apps, has the benefit of never being hidden underneath the virtual keyboard, and is always visible no matter where you're scrolled on the page.

+ + <%= form_with url: components_path, class: "flex-container row-gap-gutter", data: {controller: "bridge--form", action: "turbo:submit-start->bridge--form#submitStart turbo:submit-end->bridge--form#submitEnd"} do |form| %> +
+ <%= form.label :first_name, class: "form-label" %> + <%= form.text_field :first_name, class: "form-control inline-100" %> +
+ +
+ <%= form.label :last_name, class: "form-label" %> + <%= form.text_field :last_name, class: "form-control inline-100" %> +
+ +
+ <%= form.submit "Submit form", data: { bridge__form_target: "submit", bridge_title: "Submit" }, class: "button inline-100" %> +
+ <% end %> +
diff --git a/app/views/components/overflow.html.erb b/app/views/components/overflow.html.erb new file mode 100644 index 0000000..b4add12 --- /dev/null +++ b/app/views/components/overflow.html.erb @@ -0,0 +1,49 @@ +<% content_for :title, "Overflow Component" %> + +
+

Overflow Component

+

This screen contains a button associated with a Bridge overflow-menu component. The page also contains a web-based menu associated with a Bridge menu component that contains several options to choose from.

+

Since the Hotwire Native demo app supports the overflow-menu component and the menu component, the web button to open the menu is hidden and replaced with a native 3-dot menu in the top-right native app bar. Tapping on that 3-dot menu will display a native menu driven by the menu component.

+

Hiding the menu options behind the 3-dot overflow-menu is a typical convention in mobile apps. It provides a common place for infrequently used actions and makes more room on the screen for more important actions.

+ +
+ + +
+ +
+ + +
+

Select an option

+ + + + +
+
+ +
+
+
+
diff --git a/app/views/components/show.html.erb b/app/views/components/show.html.erb new file mode 100644 index 0000000..aea8cd0 --- /dev/null +++ b/app/views/components/show.html.erb @@ -0,0 +1,16 @@ +<% content_for :title, "Form Submitted" %> + +
+

Form Submitted

+

This page was redirected to after submitting the form. You entered the following:

+
+
+
First name:
+
<%= @first_name %>
+
+
+
Last name:
+
<%= @last_name %>
+
+
+
diff --git a/app/views/dashboards/show.html.erb b/app/views/dashboards/show.html.erb new file mode 100644 index 0000000..2b9e724 --- /dev/null +++ b/app/views/dashboards/show.html.erb @@ -0,0 +1,70 @@ +<% content_for :title, "Navigation" %> + +
+

Navigation

+

This demo app will help you get acquainted with Hotwire Native.

+ +
+ <%= render "navigations/item", + path: navigation_path, + icon: "arrow-right-bold", + name: "Basic navigation", + description: "Push a screen on the stack." %> + + <%= render "navigations/item", + path: new_modal_path, + icon: "arrow-up-bold", + name: "Modal navigation", + description: "Present a modal screen." %> + + <%= render "navigations/item", + path: slow_navigation_path, + icon: "hourglass-bold", + name: "Slow-loading page", + description: "See the loading indicator in action." %> + + <%= render "navigations/item", + path: "/not_found", + icon: "bug-bold", + name: "Error handling", + description: "Visit a page that does not exist (404)." %> +
+ +

Advanced Navigation

+ +
+ <%= render "navigations/item", + path: numbers_path, + icon: "smartphone-bold", + name: "Native screen", + description: "Intercept with a native view." %> + + <%= render "navigations/item", + path: redirect_navigation_path, + icon: "arrow-up-right-bold", + name: "Redirect", + description: "Follow an internal redirect." %> +
+ +

External Navigation

+ +
+ <%= render "navigations/item", + path: docs_url, + icon: "external-link-bold", + name: "External link", + description: "Visit a page with a different host." %> + + <%= render "navigations/item", + path: external_redirect_path, + icon: "arrow-up-right-bold", + name: "External redirect", + description: "Follow an external redirect." %> + + <%= render "navigations/item", + path: "sms:555-555-5555", + icon: "speech-bubble-bold", + name: "sms: URLs".html_safe, + description: "Handle non-http(s) URLs natively." %> +
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..b8cfff3 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,31 @@ + + + + <%= content_for(:title) || "Hotwire Native Demo" %> + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= yield :head %> + + <%= stylesheet_link_tag "variables", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "form", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "containers", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "header", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "utilities", "data-turbo-track": "reload" %> + <%= stylesheet_link_tag "native", "data-turbo-track": "reload" if hotwire_native_app? %> + <%= javascript_importmap_tags %> + + + + + + + <%= render "shared/nav" %> +
+ <%= render "shared/flash" %> + <%= yield %> +
+ + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..3aac900 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/modals/new.html.erb b/app/views/modals/new.html.erb new file mode 100644 index 0000000..3bdac9e --- /dev/null +++ b/app/views/modals/new.html.erb @@ -0,0 +1,59 @@ +<% content_for :title, "Modal Navigation" %> + +
+

Modal Navigation

+

+ This screen was presented as a modal.
+ Triggered via the following Path Configuration rule:
+

{
+  "patterns": [
+    "/new$"
+  ],
+  "properties": {
+    "context": "modal"
+  }
+}
+

+ +
+ <%= render "navigations/item", + path: navigation_path, + icon: "arrow-return-bold", + name: "Basic navigation", + description: "Dismiss the modal and push a screen on the main stack." %> + + <%= render "navigations/item", + path: modal_path, + icon: "arrow-right-bold", + name: "Modal navigation", + description: "Push a screen on the modal stack." %> +
+ +

Historical Navigation

+ +
+ <%= render "navigations/item", + path: turbo_recede_historical_location_path, + icon: "arrow-down-bold", + name: "Recede navigation", + description: "Dismiss this modal." %> + + <%= render "navigations/item", + path: turbo_refresh_historical_location_path, + icon: "arrow-clockwise-bold", + name: "Refresh navigation", + description: "Dismiss this modal then refresh." %> + + <%= render "navigations/item", + path: redirect_modal_path, + icon: "arrow-clockwise-bold", + name: "Refresh navigation (via redirect)", + description: "Dismiss this modal then refresh." %> + + <%= render "navigations/item", + path: turbo_resume_historical_location_path, + icon: "stop-bold", + name: "Resume navigation", + description: "Do nothing." %> +
+
diff --git a/app/views/modals/replace.html.erb b/app/views/modals/replace.html.erb new file mode 100644 index 0000000..f4ddbf2 --- /dev/null +++ b/app/views/modals/replace.html.erb @@ -0,0 +1,10 @@ +<% content_for :title, "Replace" %> + +
+

Replace

+

+ This screen replaced the previous modal one.
+ Trigger this by adding the following to a link already configured to be presented modaly:
+ data: {turbo_action: :replace} +

+
diff --git a/app/views/modals/show.html.erb b/app/views/modals/show.html.erb new file mode 100644 index 0000000..9d4b93c --- /dev/null +++ b/app/views/modals/show.html.erb @@ -0,0 +1,32 @@ +<% content_for :title, "Modal Navigation #2" %> + +
+

Modal Navigation #2

+

+ This screen was pushed on the modal stack.
+ Triggered via the following Path Configuration rule:
+

{
+  "patterns": [
+    "/modal$"
+  ],
+  "properties": {
+    "context": "modal"
+  }
+}
+

+ +
+ <%= render "navigations/item", + path: replace_modal_path, + data: {turbo_action: :replace}, + icon: "arrow-left-right-bold", + name: "Replace navigation", + description: "Replace this modal screen with a new one." %> + + <%= render "navigations/item", + path: turbo_recede_historical_location_path, + icon: "arrow-left-bold", + name: "Recede navigation", + description: "Pop this screen off the modal stack." %> +
+
diff --git a/app/views/navigations/_item.html.erb b/app/views/navigations/_item.html.erb new file mode 100644 index 0000000..07f8b73 --- /dev/null +++ b/app/views/navigations/_item.html.erb @@ -0,0 +1,8 @@ +<%= link_to path, data: local_assigns[:data] || {}, class: "formatted-list__item" do %> + <%= icon_tag icon %> +
+

<%= name %>

+

<%= description %>

+
+ <%= icon_tag "chevron-right-bold", size: :small, class: "color-subtle" %> +<% end %> diff --git a/app/views/navigations/redirected.html.erb b/app/views/navigations/redirected.html.erb new file mode 100644 index 0000000..191424e --- /dev/null +++ b/app/views/navigations/redirected.html.erb @@ -0,0 +1,6 @@ +<% content_for :title, "Redirected Page" %> + +
+

Redirected Page

+

This page is the result of a redirect. The original destination has been replaced with this page.

+
diff --git a/app/views/navigations/replace.html.erb b/app/views/navigations/replace.html.erb new file mode 100644 index 0000000..e66bd9b --- /dev/null +++ b/app/views/navigations/replace.html.erb @@ -0,0 +1,10 @@ +<% content_for :title, "Replace Navigation" %> + +
+

Replace Navigation

+

+ This screen replaced the previous one.
+ Trigger this by adding the following to a link:
+ data: {turbo_action: :replace} +

+
diff --git a/app/views/navigations/second.html.erb b/app/views/navigations/second.html.erb new file mode 100644 index 0000000..b3f5e96 --- /dev/null +++ b/app/views/navigations/second.html.erb @@ -0,0 +1,20 @@ +<% content_for :title, "Basic Navigation #2" %> + +
+

Basic Navigation #2

+

Another screen pushed onto the navigation stack.

+ +
+ <%= render "navigations/item", + path: navigation_path, + icon: "arrow-left-bold", + name: "Visit previous page", + description: "Pop this screen and reload previous one." %> + + <%= render "navigations/item", + path: root_path, + icon: "arrow-to-start-bold", + name: "Clear all", + description: "Pop all screens off the stack." %> +
+
diff --git a/app/views/navigations/show.html.erb b/app/views/navigations/show.html.erb new file mode 100644 index 0000000..354a972 --- /dev/null +++ b/app/views/navigations/show.html.erb @@ -0,0 +1,52 @@ +<% content_for :title, "Basic Navigation" %> + +
+

Basic Navigation

+

+ This screen was pushed onto the navigation stack.
+ This is the default behavior, no custom options are required. +

+ +
+ <%= render "navigations/item", + path: second_navigation_path, + icon: "arrow-right-bold", + name: "Basic navigation", + description: "Push another screen on the stack." %> + + <%= render "navigations/item", + path: replace_navigation_path, + data: {turbo_action: :replace}, + icon: "arrow-left-right-bold", + name: "Replace navigation", + description: "Replace this screen with a new one." %> + + <%= render "navigations/item", + path: navigation_path, + icon: "arrow-clockwise-bold", + name: "Visit same page", + description: "Reload this screen." %> +
+ +

Historical Navigation

+ +
+ <%= render "navigations/item", + path: turbo_recede_historical_location_path, + icon: "arrow-left-bold", + name: "Recede navigation", + description: "Pop this screen off the stack." %> + + <%= render "navigations/item", + path: turbo_refresh_historical_location_path, + icon: "arrow-clockwise-bold", + name: "Refresh navigation", + description: "Pop this screen and refresh the previous." %> + + <%= render "navigations/item", + path: turbo_resume_historical_location_path, + icon: "stop-bold", + name: "Resume navigation", + description: "Do nothing." %> +
+
diff --git a/app/views/navigations/slow.html.erb b/app/views/navigations/slow.html.erb new file mode 100644 index 0000000..cade45f --- /dev/null +++ b/app/views/navigations/slow.html.erb @@ -0,0 +1,8 @@ +<% content_for :title, "Slow-Loading Page" %> + +
+

Slow-Loading Page

+

This page is rendered with a delay on the server so you can see the loading indicator and test Turbo's preview cache.

+

To see the preview cache in action, tap the Back button, then return to this page. It will load instantly while the slow network request completes in the background.

+

Tap the Back button and use pull-to-refresh before returning to see the slow, uncached version again.

+
diff --git a/app/views/numbers/index.html.erb b/app/views/numbers/index.html.erb new file mode 100644 index 0000000..b32fa42 --- /dev/null +++ b/app/views/numbers/index.html.erb @@ -0,0 +1,14 @@ +<% content_for :title, "Numbers" %> + +
+

Numbers

+

This page will be intercepted with a native screen in the apps.

+ +
+ <% (1..10).each do |i| %> +
+ <%= i %> +
+ <% end %> +
+
diff --git a/app/views/numbers/show.html.erb b/app/views/numbers/show.html.erb new file mode 100644 index 0000000..35dd31e --- /dev/null +++ b/app/views/numbers/show.html.erb @@ -0,0 +1,8 @@ +<% content_for :title, "##{@number}" %> + +
+

<%= "##{@number}" %>

+

You just navigated from a native screen to a web screen.

+

On iOS, we used a Router to pass a URL back to Hotwire Native. Check out NumbersViewController for an example.

+

On Android, we….

+
diff --git a/app/views/resources/index.html.erb b/app/views/resources/index.html.erb new file mode 100644 index 0000000..2420eb4 --- /dev/null +++ b/app/views/resources/index.html.erb @@ -0,0 +1,32 @@ +<% content_for :title, "Resources" %> + +
+

Resources

+

This page includes links to Hotwire Native resources, answers to common questions, and a place to validate bug fixes.

+ +
+ <%= render "navigations/item", + path: docs_url, + icon: "book-bold", + name: "Documentation", + description: "The official Hotwire Native docs." %> + + <%= render "navigations/item", + path: long_resources_path, + icon: "scroll-bold", + name: "Scroll restoration", + description: "Keep your place when navigating back." %> + + <%= render "navigations/item", + path: new_resource_path, + icon: "warning-bold", + name: "Forms and flash messages", + description: "A demo for forms and flash messages." %> + + <%= render "navigations/item", + path: upload_resources_path, + icon: "cloud-upload-bold", + name: "Native file uploads", + description: "A demo for image and camera uploads." %> +
+
diff --git a/app/views/resources/long.html.erb b/app/views/resources/long.html.erb new file mode 100644 index 0000000..ad4b7f7 --- /dev/null +++ b/app/views/resources/long.html.erb @@ -0,0 +1,39 @@ +<% content_for :title, "A Really Long Page" %> + +
+

A Really Long Page

+ +

Scroll down a bit, then tap a link to navigate forward to another screen.

+ +

Moby Dick

+ +

Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me.

+ +

<%= link_to "Scroll to the bottom of the screen", "#bottom", class: "btn btn-outline-primary w-100" %>

+ +

There now is your insular city of the Manhattoes, belted round by wharves as Indian isles by coral reefs—commerce surrounds it with her surf.

+ +

<%= link_to "Navigate to another screen", scroll_resources_path, class: "btn btn-outline-primary w-100" %>

+ +

Right and left, the streets take you waterward. Its extreme downtown is the battery, where that noble mole is washed by waves, and cooled by breezes, which a few hours previous were out of sight of land. Look at the crowds of water-gazers there.

+ +

<%= link_to "Navigate to another screen", scroll_resources_path, class: "btn btn-outline-primary w-100" %>

+ +

Circumambulate the city of a dreamy Sabbath afternoon. Go from Corlears Hook to Coenties Slip, and from thence, by Whitehall, northward. What do you see?—Posted like silent sentinels all around the town, stand thousands upon thousands of mortal men fixed in ocean reveries. Some leaning against the spiles; some seated upon the pier-heads; some looking over the bulwarks of ships from China; some high aloft in the rigging, as if striving to get a still better seaward peep. But these are all landsmen; of week days pent up in lath and plaster—tied to counters, nailed to benches, clinched to desks. How then is this? Are the green fields gone? What do they here?

+ +

<%= link_to "Navigate to another screen", scroll_resources_path, class: "btn btn-outline-primary w-100" %>

+ +

But look! here come more crowds, pacing straight for the water, and seemingly bound for a dive. Strange! Nothing will content them but the extremest limit of the land; loitering under the shady lee of yonder warehouses will not suffice. No. They must get just as nigh the water as they possibly can without falling in. And there they stand—miles of them—leagues. Inlanders all, they come from lanes and alleys, streets and avenues—north, east, south, and west. Yet here they all unite. Tell me, does the magnetic virtue of the needles of the compasses of all those ships attract them thither?

+ +

<%= link_to "Navigate to another screen", scroll_resources_path, class: "btn btn-outline-primary w-100" %>

+ +

Once more. Say you are in the country; in some high land of lakes. Take almost any path you please, and ten to one it carries you down in a dale, and leaves you there by a pool in the stream. There is magic in it. Let the most absent-minded of men be plunged in his deepest reveries—stand that man on his legs, set his feet a-going, and he will infallibly lead you to water, if water there be in all that region. Should you ever be athirst in the great American desert, try this experiment, if your caravan happen to be supplied with a metaphysical professor. Yes, as every one knows, meditation and water are wedded for ever.

+ +

<%= link_to "Navigate to another screen", scroll_resources_path, class: "btn btn-outline-primary w-100" %>

+ +

But here is an artist. He desires to paint you the dreamiest, shadiest, quietest, most enchanting bit of romantic landscape in all the valley of the Saco. What is the chief element he employs? There stand his trees, each with a hollow trunk, as if a hermit and a crucifix were within; and here sleeps his meadow, and there sleep his cattle; and up from yonder cottage goes a sleepy smoke. Deep into distant woodlands winds a mazy way, reaching to overlapping spurs of mountains bathed in their hill-side blue. But though the picture lies thus tranced, and though this pine-tree shakes down its sighs like leaves upon this shepherd’s head, yet all were vain, unless the shepherd’s eye were fixed upon the magic stream before him. Go visit the Prairies in June, when for scores on scores of miles you wade knee-deep among Tiger-lilies—what is the one charm wanting?—Water—there is not a drop of water there! Were Niagara but a cataract of sand, would you travel your thousand miles to see it? Why did the poor poet of Tennessee, upon suddenly receiving two handfuls of silver, deliberate whether to buy him a coat, which he sadly needed, or invest his money in a pedestrian trip to Rockaway Beach? Why is almost every robust healthy boy with a robust healthy soul in him, at some time or other crazy to go to sea? Why upon your first voyage as a passenger, did you yourself feel such a mystical vibration, when first told that you and your ship were now out of sight of land? Why did the old Persians hold the sea holy? Why did the Greeks give it a separate deity, and own brother of Jove? Surely all this is not without meaning. And still deeper the meaning of that story of Narcissus, who because he could not grasp the tormenting, mild image he saw in the fountain, plunged into it and was drowned. But that same image, we ourselves see in all rivers and oceans. It is the image of the ungraspable phantom of life; and this is the key to it all.

+ +

<%= link_to "Navigate to another screen", scroll_resources_path, class: "btn btn-outline-primary w-100" %>

+ +

Now, when I say that I am in the habit of going to sea whenever I begin to grow hazy about the eyes, and begin to be over conscious of my lungs, I do not mean to have it inferred that I ever go to sea as a passenger. For to go as a passenger you must needs have a purse, and a purse is but a rag unless you have something in it. Besides, passengers get sea-sick—grow quarrelsome—don’t sleep of nights—do not enjoy themselves much, as a general thing;—no, I never go as a passenger; nor, though I am something of a salt, do I ever go to sea as a Commodore, or a Captain, or a Cook. I abandon the glory and distinction of such offices to those who like them. For my part, I abominate all honorable respectable toils, trials, and tribulations of every kind whatsoever. It is quite as much as I can do to take care of myself, without taking care of ships, barques, brigs, schooners, and what not. And as for going as cook,—though I confess there is considerable glory in that, a cook being a sort of officer on ship-board—yet, somehow, I never fancied broiling fowls;—though once broiled, judiciously buttered, and judgmatically salted and peppered, there is no one who will speak more respectfully, not to say reverentially, of a broiled fowl than I will. It is out of the idolatrous dotings of the old Egyptians upon broiled ibis and roasted river horse, that you see the mummies of those creatures in their huge bake-houses the pyramids.

+
diff --git a/app/views/resources/new.html.erb b/app/views/resources/new.html.erb new file mode 100644 index 0000000..e45b3c0 --- /dev/null +++ b/app/views/resources/new.html.erb @@ -0,0 +1,34 @@ +<% content_for :title, "Forms and Flash Messages" %> + +
+

Forms and Flash Messages

+

This page includes a form that demonstrates passing data to the server, redirecting to a new page, and rendering error and success flash messages.

+

Check out the form bridge component to convert the web-based submit button to a native one.

+ + <% if @resource.errors.any? %> + + <% end %> + + <%= form_with model: @resource, class: "flex-container row-gap-gutter" do |form| %> +
+ <%= form.label :first_name, class: "form-label" %> + <%= form.text_field :first_name, class: "form-control inline-100" %> +
+ +
+ <%= form.label :last_name, class: "form-label" %> + <%= form.text_field :last_name, class: "form-control inline-100" %> +
+ +
+ <%= form.submit "Submit form", data: { bridge__form_target: "submit", bridge_title: "Submit" }, class: "button inline-100" %> +
+ <% end %> +
diff --git a/app/views/resources/scroll.html.erb b/app/views/resources/scroll.html.erb new file mode 100644 index 0000000..c448f52 --- /dev/null +++ b/app/views/resources/scroll.html.erb @@ -0,0 +1,6 @@ +<% content_for :title, "Restoring Your Scroll" %> + +
+

Restoring Your Scroll

+

Now, go back. You'll notice even though we're using a single web view, Turbo automatically restores the scroll position of the previous page and takes a screenshot of the current page to make the transition seamless.

+
diff --git a/app/views/resources/show.html.erb b/app/views/resources/show.html.erb new file mode 100644 index 0000000..aea8cd0 --- /dev/null +++ b/app/views/resources/show.html.erb @@ -0,0 +1,16 @@ +<% content_for :title, "Form Submitted" %> + +
+

Form Submitted

+

This page was redirected to after submitting the form. You entered the following:

+
+
+
First name:
+
<%= @first_name %>
+
+
+
Last name:
+
<%= @last_name %>
+
+
+
diff --git a/app/views/resources/upload.html.erb b/app/views/resources/upload.html.erb new file mode 100644 index 0000000..ad61204 --- /dev/null +++ b/app/views/resources/upload.html.erb @@ -0,0 +1,7 @@ +<% content_for :title, "Native File Uploads" %> + +
+

Native File Uploads

+

Prompt for file uploads, like images or camera captures, with built-in Rails form helpers like file_field_tag.

+ <%= file_field_tag :image, accept: "image/jpg,image/jpeg,image/png", class: "form-control" %> +
diff --git a/app/views/shared/_flash.html.erb b/app/views/shared/_flash.html.erb new file mode 100644 index 0000000..214aae5 --- /dev/null +++ b/app/views/shared/_flash.html.erb @@ -0,0 +1,7 @@ +<% if flash.notice.present? %> +
+ +
+<% end %> diff --git a/app/views/shared/_nav.html.erb b/app/views/shared/_nav.html.erb new file mode 100644 index 0000000..2c42344 --- /dev/null +++ b/app/views/shared/_nav.html.erb @@ -0,0 +1,18 @@ +
+ <%= link_to root_path, class: "header__brand unstyled-link color-accent" do %> + <%= sprite_tag "hotwire-native-logo", aria: { label: "Hotwire Native Demo" } %> + <% end %> + +
diff --git a/bin/brakeman b/bin/brakeman new file mode 100755 index 0000000..ace1c9b --- /dev/null +++ b/bin/brakeman @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +ARGV.unshift("--ensure-latest") + +load Gem.bin_path("brakeman", "brakeman") diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000..42c7fd7 --- /dev/null +++ b/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 =~ 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/bin/dev b/bin/dev new file mode 100755 index 0000000..5f91c20 --- /dev/null +++ b/bin/dev @@ -0,0 +1,2 @@ +#!/usr/bin/env ruby +exec "./bin/rails", "server", *ARGV diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100755 index 0000000..57567d6 --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,14 @@ +#!/bin/bash -e + +# Enable jemalloc for reduced memory usage and latency. +if [ -z "${LD_PRELOAD+x}" ]; then + LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) + export LD_PRELOAD +fi + +# If running the rails server then create or migrate existing database +if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then + ./bin/rails db:prepare +fi + +exec "${@}" diff --git a/bin/importmap b/bin/importmap new file mode 100755 index 0000000..36502ab --- /dev/null +++ b/bin/importmap @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require_relative "../config/application" +require "importmap/commands" diff --git a/bin/jobs b/bin/jobs new file mode 100755 index 0000000..dcf59f3 --- /dev/null +++ b/bin/jobs @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "solid_queue/cli" + +SolidQueue::Cli.start(ARGV) diff --git a/bin/kamal b/bin/kamal new file mode 100755 index 0000000..cbe59b9 --- /dev/null +++ b/bin/kamal @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'kamal' 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("kamal", "kamal") diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..efc0377 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path("../config/application", __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 0000000..40330c0 --- /dev/null +++ b/bin/rubocop @@ -0,0 +1,8 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +# explicit rubocop config increases performance slightly while avoiding config confusion. +ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) + +load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..be3db3c --- /dev/null +++ b/bin/setup @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +require "fileutils" + +APP_ROOT = File.expand_path("..", __dir__) + +def system!(*args) + system(*args, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system("bundle check") || system!("bundle install") + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # FileUtils.cp "config/database.yml.sample", "config/database.yml" + # end + + puts "\n== Preparing database ==" + system! "bin/rails db:prepare" + + puts "\n== Removing old logs and tempfiles ==" + system! "bin/rails log:clear tmp:clear" + + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end +end diff --git a/bin/thrust b/bin/thrust new file mode 100755 index 0000000..36bde2d --- /dev/null +++ b/bin/thrust @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("thruster", "thrust") diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..4a3c09a --- /dev/null +++ b/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..a5023f9 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,27 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module HotwireNativeDemo + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 8.0 + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w[assets tasks]) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..988a5dd --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..b9adc5a --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,17 @@ +# Async adapter only works within the same process, so for manually triggering cable updates from a console, +# and seeing results in the browser, you must do so from the web console (running inside the dev process), +# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view +# to make the web console appear. +development: + adapter: async + +test: + adapter: test + +production: + adapter: solid_cable + connects_to: + database: + writing: cable + polling_interval: 0.1.seconds + message_retention: 1.day diff --git a/config/cache.yml b/config/cache.yml new file mode 100644 index 0000000..19d4908 --- /dev/null +++ b/config/cache.yml @@ -0,0 +1,16 @@ +default: &default + store_options: + # Cap age of oldest cache entry to fulfill retention policies + # max_age: <%= 60.days.to_i %> + max_size: <%= 256.megabytes %> + namespace: <%= Rails.env %> + +development: + <<: *default + +test: + <<: *default + +production: + database: cache + <<: *default diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000..d4e5192 --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +Pos8vazazsxszHGKBWJ7ybpBE4VOe7zxPz1p+Dz38GvkXYrbJby0uokCqXQYs8oInIPl8mVRqeGkRf1cnFlVYZE3EdLZtOu8CpGYLUGGoPbI2D9tTQaiOQYGC35NeNxgEPU18NScVWWgP3Vu+SYD+bJVWMTe4IfpTedvONsXIviFINbGmXQZxm6if1S8zgh/nstO9ZykVlAdRtRvQ0yp68GTBrYaFmXOfFNFrGHZRVG+Hz1r2TrUvQDnv2dhRb98L+oOgGIMofDiJEXfyXqc6Zm5MgfdbpYGzzrCBArikV9WIY1lnTijox8xbIJDAizNqLX7sb6T9M1K1uUwGk0JdILm2g1k/ivgU46T5OJeIDafwfnueSVKCvegIskMiLOwFc4E3igYKDtYtCyK9kaZQkYBJo35SSu0mvanrssK6eCYu7xxEm0GRiek4R1mUZfA66TitVcrXdXAHCUnisHbEgUvdE3xi5ITquVyNVFwTGwFpavUWq3TQTwN--uY5BI7dxpxSDRuTB--kjm1xfCVkxVAlFcGk645oQ== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..2640cb5 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,41 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem "sqlite3" +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: storage/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: storage/test.sqlite3 + + +# Store production database in the storage/ directory, which by default +# is mounted as a persistent Docker volume in config/deploy.yml. +production: + primary: + <<: *default + database: storage/production.sqlite3 + cache: + <<: *default + database: storage/production_cache.sqlite3 + migrations_paths: db/cache_migrate + queue: + <<: *default + database: storage/production_queue.sqlite3 + migrations_paths: db/queue_migrate + cable: + <<: *default + database: storage/production_cable.sqlite3 + migrations_paths: db/cable_migrate diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 0000000..4e82a59 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,116 @@ +# Name of your application. Used to uniquely configure containers. +service: hotwire_native_demo + +# Name of the container image. +image: your-user/hotwire_native_demo + +# Deploy to these servers. +servers: + web: + - 192.168.0.1 + # job: + # hosts: + # - 192.168.0.1 + # cmd: bin/jobs + +# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. +# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. +# +# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. +proxy: + ssl: true + host: app.example.com + +# Credentials for your image host. +registry: + # Specify the registry server, if you're not using Docker Hub + # server: registry.digitalocean.com / ghcr.io / ... + username: your-user + + # Always use an access token rather than real password when possible. + password: + - KAMAL_REGISTRY_PASSWORD + +# Inject ENV variables into containers (secrets come from .kamal/secrets). +env: + secret: + - RAILS_MASTER_KEY + clear: + # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. + # When you start using multiple servers, you should split out job processing to a dedicated machine. + SOLID_QUEUE_IN_PUMA: true + + # Set number of processes dedicated to Solid Queue (default: 1) + # JOB_CONCURRENCY: 3 + + # Set number of cores available to the application on each server (default: 1). + # WEB_CONCURRENCY: 2 + + # Match this to any external database server to configure Active Record correctly + # Use hotwire_native_demo-db for a db accessory server on same machine via local kamal docker network. + # DB_HOST: 192.168.0.2 + + # Log everything from Rails + # RAILS_LOG_LEVEL: debug + +# Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: +# "bin/kamal logs -r job" will tail logs from the first server in the job section. +aliases: + console: app exec --interactive --reuse "bin/rails console" + shell: app exec --interactive --reuse "bash" + logs: app logs -f + dbc: app exec --interactive --reuse "bin/rails dbconsole" + + +# Use a persistent storage volume for sqlite database files and local Active Storage files. +# Recommended to change this to a mounted volume path that is backed up off server. +volumes: + - "hotwire_native_demo_storage:/rails/storage" + + +# Bridge fingerprinted assets, like JS and CSS, between versions to avoid +# hitting 404 on in-flight requests. Combines all files from new and old +# version inside the asset_path. +asset_path: /rails/public/assets + +# Configure the image builder. +builder: + arch: amd64 + + # # Build image via remote server (useful for faster amd64 builds on arm64 computers) + # remote: ssh://docker@docker-builder-server + # + # # Pass arguments and secrets to the Docker build process + # args: + # RUBY_VERSION: 3.2.2 + # secrets: + # - GITHUB_TOKEN + # - RAILS_MASTER_KEY + +# Use a different ssh user than root +# ssh: +# user: app + +# Use accessory services (secrets come from .kamal/secrets). +# accessories: +# db: +# image: mysql:8.0 +# host: 192.168.0.2 +# # Change to 3306 to expose port to the world instead of just local network. +# port: "127.0.0.1:3306:3306" +# env: +# clear: +# MYSQL_ROOT_HOST: '%' +# secret: +# - MYSQL_ROOT_PASSWORD +# files: +# - config/mysql/production.cnf:/etc/mysql/my.cnf +# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql +# directories: +# - data:/var/lib/mysql +# redis: +# image: redis:7.0 +# host: 192.168.0.2 +# port: 6379 +# directories: +# - data:/data diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..4cc21c4 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,72 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Make code changes take effect immediately without server restart. + config.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing. + config.server_timing = true + + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. + if Rails.root.join("tmp/caching-dev.txt").exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } + else + config.action_controller.perform_caching = false + end + + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + # Make template changes take effect immediately. + config.action_mailer.perform_caching = false + + # Set localhost to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "localhost", port: 3000 } + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + + # Highlight code that enqueued background job in logs. + config.active_job.verbose_enqueue_logs = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + config.action_view.annotate_rendered_view_with_filenames = true + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true + + # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. + # config.generators.apply_rubocop_autocorrect_after_generate! +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..bdcd01d --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,90 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). + config.eager_load = true + + # Full error reports are disabled. + config.consider_all_requests_local = false + + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true + + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = "http://assets.example.com" + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Skip http-to-https redirect for the default health check endpoint. + # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } + + # Log to STDOUT with the current request id as a default log tag. + config.log_tags = [ :request_id ] + config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) + + # Change to "debug" to log everything (including potentially personally-identifiable information!) + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. + config.cache_store = :solid_cache_store + + # Replace the default in-process and non-durable queuing backend for Active Job. + config.active_job.queue_adapter = :solid_queue + config.solid_queue.connects_to = { database: { writing: :queue } } + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Only use :id for inspections in production. + config.active_record.attributes_for_inspect = [ :id ] + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..c2095b1 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,53 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } + + # Show full error reports. + config.consider_all_requests_local = true + config.cache_store = :null_store + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Raise error when a before_action's only/except options reference missing actions. + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000..67110a3 --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,8 @@ +# Pin npm packages by running ./bin/importmap + +pin "application" +pin "@hotwired/turbo-rails", to: "turbo.min.js" +pin "@hotwired/stimulus", to: "@hotwired--stimulus.js" # @3.2.2 +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" +pin_all_from "app/javascript/controllers", under: "controllers" +pin "@hotwired/hotwire-native-bridge", to: "@hotwired--hotwire-native-bridge.js" # @1.0.0 diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000..4873244 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = "1.0" + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..b3076b3 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..c0b717f --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..3860f65 --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, "\\1en" +# inflect.singular /^(ox)en/i, "\\1" +# inflect.irregular "person", "people" +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym "RESTful" +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..6c349ae --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..a248513 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,41 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. +# +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3000) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Run the Solid Queue supervisor inside of Puma for single-server deployments +plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/config/queue.yml b/config/queue.yml new file mode 100644 index 0000000..9eace59 --- /dev/null +++ b/config/queue.yml @@ -0,0 +1,18 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/config/recurring.yml b/config/recurring.yml new file mode 100644 index 0000000..d045b19 --- /dev/null +++ b/config/recurring.yml @@ -0,0 +1,10 @@ +# production: +# periodic_cleanup: +# class: CleanSoftDeletedRecordsJob +# queue: background +# args: [ 1000, { batch_size: 500 } ] +# schedule: every hour +# periodic_command: +# command: "SoftDeletedRecord.due.delete_all" +# priority: 2 +# schedule: at 5am every day diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..9e67872 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,57 @@ +Rails.application.routes.draw do + resources :bugs, only: :index do + collection do + get :tabs + end + end + + resources :components, only: %i[index new create] do + collection do + get :menu + get :overflow + end + end + get "/component", to: "components#show", as: :component + + resources :configurations, only: [] do + get "ios_v1", on: :collection + end + + resource :dashboards, only: :show + + resource :modal, only: %i[new show] do + collection do + get :redirect + get :replace + end + end + + resource :navigation, only: :show do + collection do + get :redirect + get :redirected + get :replace + get :slow + get :second + end + end + + resources :numbers, only: %i[index show] + + resources :resources, only: %i[index new create] do + collection do + get :long + get :scroll + get :upload + end + end + get "/resource", to: "resources#show", as: :resource + + direct(:docs) { "https://native.hotwired.dev" } + direct(:bridge_components) { "https://native.hotwired.dev/overview/bridge-components" } + + get :external_redirect, to: redirect("https://37signals.com") + + # Defines the root path route ("/") + root "dashboards#show" +end diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000..4942ab6 --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/db/cable_schema.rb b/db/cable_schema.rb new file mode 100644 index 0000000..2366660 --- /dev/null +++ b/db/cable_schema.rb @@ -0,0 +1,11 @@ +ActiveRecord::Schema[7.1].define(version: 1) do + create_table "solid_cable_messages", force: :cascade do |t| + t.binary "channel", limit: 1024, null: false + t.binary "payload", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "channel_hash", limit: 8, null: false + t.index ["channel"], name: "index_solid_cable_messages_on_channel" + t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" + t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" + end +end diff --git a/db/cache_schema.rb b/db/cache_schema.rb new file mode 100644 index 0000000..6005a29 --- /dev/null +++ b/db/cache_schema.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +ActiveRecord::Schema[7.2].define(version: 1) do + create_table "solid_cache_entries", force: :cascade do |t| + t.binary "key", limit: 1024, null: false + t.binary "value", limit: 536870912, null: false + t.datetime "created_at", null: false + t.integer "key_hash", limit: 8, null: false + t.integer "byte_size", limit: 4, null: false + t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" + t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" + t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true + end +end diff --git a/db/queue_schema.rb b/db/queue_schema.rb new file mode 100644 index 0000000..85194b6 --- /dev/null +++ b/db/queue_schema.rb @@ -0,0 +1,129 @@ +ActiveRecord::Schema[7.1].define(version: 1) do + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.string "concurrency_key", null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" + t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.bigint "process_id" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.text "error" + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "queue_name", null: false + t.string "class_name", null: false + t.text "arguments" + t.integer "priority", default: 0, null: false + t.string "active_job_id" + t.datetime "scheduled_at" + t.datetime "finished_at" + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" + t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" + t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" + t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" + t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.string "queue_name", null: false + t.datetime "created_at", null: false + t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.bigint "supervisor_id" + t.integer "pid", null: false + t.string "hostname" + t.text "metadata" + t.datetime "created_at", null: false + t.string "name", null: false + t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" + t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "task_key", null: false + t.datetime "run_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_recurring_tasks", force: :cascade do |t| + t.string "key", null: false + t.string "schedule", null: false + t.string "command", limit: 2048 + t.string "class_name" + t.text "arguments" + t.string "queue_name" + t.integer "priority", default: 0 + t.boolean "static", default: true, null: false + t.text "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.bigint "job_id", null: false + t.string "queue_name", null: false + t.integer "priority", default: 0, null: false + t.datetime "scheduled_at", null: false + t.datetime "created_at", null: false + t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.string "key", null: false + t.integer "value", default: 1, null: false + t.datetime "expires_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" + t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" + t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true + end + + add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..4fbd6ed --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,9 @@ +# This file should ensure the existence of records required to run the application in every environment (production, +# development, test). The code here should be idempotent so that it can be executed at any point in every environment. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Example: +# +# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| +# MovieGenre.find_or_create_by!(name: genre_name) +# end diff --git a/json/reference.json b/json/reference.json deleted file mode 100644 index 83daa44..0000000 --- a/json/reference.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "subtitle": "Reference", - "items": [ - { - "id": 1, - "title": "Turbo Drive", - "description": "Turbo Drive accelerates links and form submissions by negating the need for full page reloads.", - "path": "/reference/turbo-drive", - "icon_path": "/images/turbo-drive.png" - }, - { - "id": 2, - "title": "Turbo Frames", - "description": "Turbo Frames decompose pages into independent contexts, which scope navigation and can be lazily loaded.", - "path": "/reference/turbo-frames", - "icon_path": "/images/turbo-frames.png" - }, - { - "id": 3, - "title": "Turbo Streams", - "description": "Turbo Streams deliver page changes over WebSocket, SSE or in response to form submissions using just HTML and a set of CRUD-like actions.", - "path": "/reference/turbo-streams", - "icon_path": "/images/turbo-streams.png" - }, - { - "id": 4, - "title": "Turbo Native", - "description": "Turbo Native lets your majestic monolith form the center of your native iOS and Android apps, with seamless transitions between web and native sections.", - "path": "/reference/turbo-native", - "icon_path": "/images/turbo-native.png" - } - ] -} diff --git a/json/reference/turbo-drive.json b/json/reference/turbo-drive.json deleted file mode 100644 index 5e92cc7..0000000 --- a/json/reference/turbo-drive.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Turbo Drive: Navigate within a persistent process", - "content": "A key attraction with traditional single-page applications, when compared with the old-school, separate-pages approach, is the speed of navigation. SPAs get a lot of that speed from not constantly tearing down the application process, only to reinitialize it on the very next page.\n\nTurbo Drive gives you that same speed by using the same persistent-process model, but without requiring you to craft your entire application around the paradigm. There’s no client-side router to maintain, there’s no state to carefully manage. The persistent process is managed by Turbo, and you write your server-side code as though you were living back in the early aughts – blissfully isolated from the complexities of today’s SPA monstrosities!\n\nThis happens by intercepting all clicks on links to the same domain. When you click an eligible link, Turbo Drive prevents the browser from following it, changes the browser’s URL using the History API, requests the new page using fetch, and then renders the HTML response.\n\nSame deal with forms. Their submissions are turned into fetch requests from which Turbo Drive will follow the redirect and render the HTML response.\n\nDuring rendering, Turbo Drive replaces the current 'body' element outright and merges the contents of the 'head' element. The JavaScript window and document objects, and the 'html' element, persist from one rendering to the next.\n\nWhile it’s possible to interact directly with Turbo Drive to control how visits happen or hook into the lifecycle of the request, the majority of the time this is a drop-in replacement where the speed is free just by adopting a few conventions.", - "icon_path": "/images/turbo-drive.png", - "external_link": { - "title": "Navigate with Turbo Drive", - "url": "https://turbo.hotwired.dev/handbook/drive" - } -} diff --git a/json/reference/turbo-frames.json b/json/reference/turbo-frames.json deleted file mode 100644 index d7fa6ae..0000000 --- a/json/reference/turbo-frames.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Turbo Frames: Decompose complex pages", - "content": "Most web applications present pages that contain several independent segments. For a discussion page, you might have a navigation bar on the top, a list of messages in the center, a form at the bottom to add a new message, and a sidebar with related topics. Generating this discussion page normally means generating each segment in a serialized manner, piecing them all together, then delivering the result as a single HTML response to the browser.\n\nWith Turbo Frames, you can place those independent segments inside frame elements that can scope their navigation and be lazily loaded. Scoped navigation means all interaction within a frame, like clicking links or submitting forms, happens within that frame, keeping the rest of the page from changing or reloading.\n\nTurbo Frames affords you:\n\nEfficient caching. In the discussion page example above, the related topics sidebar needs to expire whenever a new related topic appears, but the list of messages in the center does not. When everything is just one page, the whole cache expires as soon as any of the individual segments do. With frames, each segment is cached independently, so you get longer-lived caches with fewer dependent keys.\n\nParallelized execution. Each defer-loaded frame is generated by its own HTTP request/response, which means it can be handled by a separate process. This allows for parallelized execution without having to manually manage the process. A complicated composite page that takes 400ms to complete end-to-end can be broken up with frames where the initial request might only take 50ms, and each of three defer-loaded frames each take 50ms. Now the whole page is done in 100ms because the three frames each taking 50ms run concurrently rather than sequentially.\n\nReady for mobile. In mobile apps, you usually can’t have big, complicated composite pages. Each segment needs a dedicated screen. With an application built using Turbo Frames, you’ve already done this work of turning the composite page into segments. These segments can then appear in native sheets and screens without alteration (since they all have independent URLs).", - "icon_path": "/images/turbo-frames.png", - "external_link": { - "title": "Decompose with Turbo Frames", - "url": "https://turbo.hotwired.dev/handbook/frames" - } -} diff --git a/json/reference/turbo-native.json b/json/reference/turbo-native.json deleted file mode 100644 index 30a5a6d..0000000 --- a/json/reference/turbo-native.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Turbo Native: Hybrid apps for iOS & Android", - "content": "Turbo Native is ideal for building hybrid apps for iOS and Android. You can use your existing server-rendered HTML to get baseline coverage of your app’s functionality in a native wrapper. Then you can spend all the time you saved on making the few screens that really benefit from high-fidelity native controls even better.\n\nAn application like Basecamp has hundreds of screens. Rewriting every single one of those screens would be an enormous task with very little benefit. Better to reserve the native firepower for high-touch interactions that really demand the highest fidelity. Something like the “New For You” inbox in Basecamp, for example, where we use swipe controls that need to feel just right. But most pages, like the one showing a single message, wouldn’t really be any better if they were completely native.\n\nGoing hybrid doesn’t just speed up your development process, it also gives you more freedom to upgrade your app without going through the slow and onerous app store release processes. Anything that’s done in HTML can be changed in your web application, and instantly be available to all users. No waiting for Big Tech to approve your changes, no waiting for users to upgrade.\n\nTurbo Native assumes you’re using the recommended development practices available for iOS and Android. This is not a framework that abstracts native APIs away or even tries to let your native code be shareable between platforms. The part that’s shareable is the HTML that’s rendered server-side. But the native controls are written in the recommended native APIs.", - "icon_path": "/images/turbo-native.png", - "external_link": { - "title": "Go Native on iOS & Android", - "url": "https://turbo.hotwired.dev/handbook/native" - } -} diff --git a/json/reference/turbo-streams.json b/json/reference/turbo-streams.json deleted file mode 100644 index a1534f2..0000000 --- a/json/reference/turbo-streams.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "title": "Turbo Streams: Deliver live page changes", - "content": "Making partial page changes in response to asynchronous actions is how we make the application feel alive. While Turbo Frames give us such updates in response to direct interactions within a single frame, Turbo Streams let us change any part of the page in response to updates sent over a WebSocket connection, SSE or other transport. (Think an imbox that automatically updates when a new email arrives.)\n\nTurbo Streams introduces a 'turbo-stream' element with seven basic actions: append, prepend, replace, update, remove, before, and after. With these actions, along with the target attribute specifying the ID of the element you want to operate on, you can encode all the mutations needed to refresh the page. You can even combine several stream elements in a single stream message. Simply include the HTML you’re interested in inserting or replacing in a template tag and Turbo does the rest.\n\nReuse the server-side templates: Live page changes are generated using the same server-side templates that were used to create the first-load page.\n\nHTML over the wire: Since all we’re sending is HTML, you don’t need any client-side JavaScript (beyond Turbo, of course) to process the update. Yes, the HTML payload might be a tad larger than a comparable JSON, but with gzip, the difference is usually negligible, and you save all the client-side effort it takes to fetch JSON and turn it into HTML.Simpler control flow: It’s really clear to follow what happens when messages arrive on the WebSocket, SSE or in response to form submissions. There’s no routing, event bubbling, or other indirection required. It’s just the HTML to be changed, wrapped in a single tag that tells us how.\n\nNow, unlike RJS and SJR, it’s not possible to call custom JavaScript functions as part of a Turbo Streams action. But this is a feature, not a bug. Those techniques can easily end up producing a tangled mess when way too much JavaScript is sent along with the response. Turbo focuses squarely on just updating the DOM, and then assumes you’ll connect any additional behavior using Stimulus actions and lifecycle callbacks.", - "icon_path": "/images/turbo-streams.png", - "external_link": { - "title": "Come Alive with Turbo Streams", - "url": "https://turbo.hotwired.dev/handbook/streams" - } -} diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 51fcad6..0000000 --- a/package-lock.json +++ /dev/null @@ -1,1367 +0,0 @@ -{ - "name": "hotwire-native-demo", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "hotwire-native-demo", - "version": "0.0.1", - "license": "MIT", - "dependencies": { - "body-parser": "^1.20.2", - "cookie-parser": "^1.4.6", - "ejs": "^3.1.9", - "express": "^4.18.2", - "express-ejs-layouts": "^2.5.1" - }, - "devDependencies": { - "multer": "^1.4.5-lts.1", - "nodemon": "^3.1.0" - }, - "engines": { - "node": "18.x" - } - }, - "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==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "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==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "dev": true - }, - "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==" - }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" - }, - "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==" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "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.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "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, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dev": true, - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "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/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "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/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==", - "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==" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "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==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", - "dependencies": { - "cookie": "0.4.1", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "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 - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "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==", - "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/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "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==" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "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==", - "engines": { - "node": ">= 0.4" - } - }, - "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==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "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" - } - }, - "node_modules/express-ejs-layouts": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz", - "integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA==" - }, - "node_modules/express/node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "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, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "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==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "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==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "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 - }, - "node_modules/jake": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", - "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", - "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", - "filelist": "^1.0.4", - "minimatch": "^3.1.2" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "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, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/multer": { - "version": "1.4.5-lts.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", - "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", - "dev": true, - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "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==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nodemon": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.3.tgz", - "integrity": "sha512-m4Vqs+APdKzDFpuaL9F9EVOF85+h070FnkHVEoU4+rmT6Vw0bmNl7s61VEkY/cJkL7RCv1p4urnUDUMrS5rk2w==", - "dev": true, - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/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, - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nodemon/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, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "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 - }, - "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==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "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, - "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/readable-stream/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 - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "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" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/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==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "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==", - "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==" - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, - "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, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/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 - }, - "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==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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, - "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==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "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==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "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 - }, - "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==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "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, - "engines": { - "node": ">=0.4" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 83e9f97..0000000 --- a/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "hotwire-native-demo", - "version": "0.0.1", - "description": "A demo app for Hotwire Native apps", - "main": "server.js", - "type": "module", - "scripts": { - "start": "node server.js" - }, - "dependencies": { - "body-parser": "^1.20.2", - "cookie-parser": "^1.4.6", - "ejs": "^3.1.9", - "express": "^4.18.2", - "express-ejs-layouts": "^2.5.1" - }, - "engines": { - "node": "18.x" - }, - "repository": { - "url": "https://github.com/hotwired/hotwire-native-demo" - }, - "license": "MIT", - "devDependencies": { - "multer": "^1.4.5-lts.1", - "nodemon": "^3.1.0" - } -} diff --git a/public/400.html b/public/400.html new file mode 100644 index 0000000..282dbc8 --- /dev/null +++ b/public/400.html @@ -0,0 +1,114 @@ + + + + + + + The server cannot process the request due to a client error (400 Bad Request) + + + + + + + + + + + + + +
+
+ +
+
+

The server cannot process the request due to a client error. Please check the request and try again. If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..c0670bc --- /dev/null +++ b/public/404.html @@ -0,0 +1,114 @@ + + + + + + + The page you were looking for doesn’t exist (404 Not found) + + + + + + + + + + + + + +
+
+ +
+
+

The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/public/406-unsupported-browser.html b/public/406-unsupported-browser.html new file mode 100644 index 0000000..9532a9c --- /dev/null +++ b/public/406-unsupported-browser.html @@ -0,0 +1,114 @@ + + + + + + + Your browser is not supported (406 Not Acceptable) + + + + + + + + + + + + + +
+
+ +
+
+

Your browser is not supported.
Please upgrade your browser to continue.

+
+
+ + + + diff --git a/public/422.html b/public/422.html new file mode 100644 index 0000000..8bcf060 --- /dev/null +++ b/public/422.html @@ -0,0 +1,114 @@ + + + + + + + The change you wanted was rejected (422 Unprocessable Entity) + + + + + + + + + + + + + +
+
+ +
+
+

The change you wanted was rejected. Maybe you tried to change something you didn’t have access to. If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000..d77718c --- /dev/null +++ b/public/500.html @@ -0,0 +1,114 @@ + + + + + + + We’re sorry, but something went wrong (500 Internal Server Error) + + + + + + + + + + + + + +
+
+ +
+
+

We’re sorry, but something went wrong.
If you’re the application owner check the logs for more information.

+
+
+ + + + diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..c4c9dbf Binary files /dev/null and b/public/icon.png differ diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 0000000..04b34bf --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/disclosure.svg b/public/images/disclosure.svg deleted file mode 100644 index 6cbae9d..0000000 --- a/public/images/disclosure.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/images/error.svg b/public/images/error.svg deleted file mode 100644 index e0c68e9..0000000 --- a/public/images/error.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/public/images/external.svg b/public/images/external.svg deleted file mode 100644 index da51e20..0000000 --- a/public/images/external.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/images/files.svg b/public/images/files.svg deleted file mode 100644 index 114dd28..0000000 --- a/public/images/files.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/public/images/image.jpg b/public/images/image.jpg deleted file mode 100644 index 4770a45..0000000 Binary files a/public/images/image.jpg and /dev/null differ diff --git a/public/images/image_thumbnail.jpg b/public/images/image_thumbnail.jpg deleted file mode 100644 index 43d1a5c..0000000 Binary files a/public/images/image_thumbnail.jpg and /dev/null differ diff --git a/public/images/javascript.svg b/public/images/javascript.svg deleted file mode 100644 index d39e079..0000000 --- a/public/images/javascript.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/public/images/link.svg b/public/images/link.svg deleted file mode 100644 index e8642d6..0000000 --- a/public/images/link.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/public/images/modal.svg b/public/images/modal.svg deleted file mode 100644 index 990a5f8..0000000 --- a/public/images/modal.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/public/images/native.svg b/public/images/native.svg deleted file mode 100644 index f52368c..0000000 --- a/public/images/native.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/navigate.svg b/public/images/navigate.svg deleted file mode 100644 index 63cdba4..0000000 --- a/public/images/navigate.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/images/protected.svg b/public/images/protected.svg deleted file mode 100644 index 1d80ba8..0000000 --- a/public/images/protected.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/public/images/redirect.svg b/public/images/redirect.svg deleted file mode 100644 index 14303a8..0000000 --- a/public/images/redirect.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/images/reference.svg b/public/images/reference.svg deleted file mode 100644 index 5fdb677..0000000 --- a/public/images/reference.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - diff --git a/public/images/scroll.svg b/public/images/scroll.svg deleted file mode 100644 index 659e9b3..0000000 --- a/public/images/scroll.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/public/images/slow.svg b/public/images/slow.svg deleted file mode 100644 index 2170ec1..0000000 --- a/public/images/slow.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/public/images/turbo-drive.png b/public/images/turbo-drive.png deleted file mode 100644 index 6f05fd6..0000000 Binary files a/public/images/turbo-drive.png and /dev/null differ diff --git a/public/images/turbo-drive.svg b/public/images/turbo-drive.svg deleted file mode 100644 index fd29e99..0000000 --- a/public/images/turbo-drive.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - diff --git a/public/images/turbo-frames.png b/public/images/turbo-frames.png deleted file mode 100644 index 9ccea18..0000000 Binary files a/public/images/turbo-frames.png and /dev/null differ diff --git a/public/images/turbo-frames.svg b/public/images/turbo-frames.svg deleted file mode 100644 index 16644af..0000000 --- a/public/images/turbo-frames.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/public/images/turbo-native.png b/public/images/turbo-native.png deleted file mode 100644 index de08a37..0000000 Binary files a/public/images/turbo-native.png and /dev/null differ diff --git a/public/images/turbo-native.svg b/public/images/turbo-native.svg deleted file mode 100644 index e9d09f3..0000000 --- a/public/images/turbo-native.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/public/images/turbo-streams.png b/public/images/turbo-streams.png deleted file mode 100644 index bb36def..0000000 Binary files a/public/images/turbo-streams.png and /dev/null differ diff --git a/public/images/turbo-streams.svg b/public/images/turbo-streams.svg deleted file mode 100644 index f537425..0000000 --- a/public/images/turbo-streams.svg +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - diff --git a/public/javascript/application.js b/public/javascript/application.js deleted file mode 100644 index 5395129..0000000 --- a/public/javascript/application.js +++ /dev/null @@ -1,22 +0,0 @@ -import "@hotwired/turbo" -import { Application } from "@hotwired/stimulus" -import "@hotwired/hotwire-native-bridge" - -// Controllers -import MenuController from "./controllers/menu_controller.js" - -// Bridge Components -import BridgeFormController from "./controllers/bridge/form_controller.js" -import BridgeMenuController from "./controllers/bridge/menu_controller.js" -import BridgeOverflowMenuController from "./controllers/bridge/overflow_menu_controller.js" - -// Start Stimulus -window.Stimulus = Application.start() - -// Register Controllers -Stimulus.register("menu", MenuController) - -// Register Bridge Components -Stimulus.register("bridge--form", BridgeFormController) -Stimulus.register("bridge--menu", BridgeMenuController) -Stimulus.register("bridge--overflow-menu", BridgeOverflowMenuController) diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..c19f78a --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/public/styles/app.css b/public/styles/app.css deleted file mode 100644 index b95d1e3..0000000 --- a/public/styles/app.css +++ /dev/null @@ -1,271 +0,0 @@ -body.index { - background-color: var(--color-background-index); -} - -body:not(.index) .page-title { - margin: -0.5em 0 0.5em; - padding: var(--space-m); - background: var(--color-neutral); - text-align: center; - grid-column: 1/span 3 !important; -} - -@media (min-width: 45em) { - body:not(.index) .page-title { - margin: -1em 0 1em; - padding: var(--space-l); - grid-column: 1/span 14 !important; - } -} - -@media (min-width: 45em) { - .actions__body { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - align-items: stretch; - } -} - -@media(max-width: 44.9em) { - .actions__body { - padding-block: 0.3em; - border-radius: 0 0.7em 0.7em 0.7em; - background: var(--color-sheet); - } -} - -.actions__header { - color: var(--color-text-subtle) -} - -@media(max-width: 44.9em) { - .actions__header { - font-size: var(--type-m); - } -} - -.actions__item, -.actions__item:visited { - display: block; - padding: 0.7em 0.7em 0.7em 3em; - color: var(--color-text); - font-weight: 500; - position: relative; - text-decoration: initial; -} - -.actions__item p { - margin: var(--space-s) 0; - font-size: var(--type-xs); - line-height: var(--leading-m) !important; - text-align: left; -} - -@media (max-width: 44.9em) { - .actions__item + .actions__item:before { - content: ""; - position: absolute; - top: 0; - right: 0.7em; - left: 3em; - border-top: 0.5px solid var(--color-border); - } - - .actions__item:first-child { - border-radius: 0.5em 0.5em 0 0; - } - - .actions__item:last-child { - border-radius: 0 0 0.5em 0.5em; - } - - .actions__item { - text-decoration: none; - } -} - -@media (min-width: 45em) { - .actions__item { - width: 49%; - margin-bottom: 2%; - padding: 1.5em; - background: var(--color-sheet); - border: 0; - border-radius: 0.5em; - text-align: center; - } -} - -.actions__icon { - position: absolute; - left: 1em; - width: 1.2em; - height: 1.2em; - background-color: var(--color-tint); -} - -@media (max-width: 44.9em) { - h1 .actions__icon { - display: block; - position: static; - width: 2em; - height: 2em; - margin: 0 auto 0.5em auto; - } -} - -@media (min-width: 45em) { - .actions__icon { - display: block; - position: static; - width: 3em; - height: 3em; - margin: 0 auto 1em auto; - } -} - -@media(hover: hover) { - .actions__item:hover .actions__icon { - background-color: var(--color-tint-alt); - } -} - -.actions__icon.--navigate { - -webkit-mask: url("/images/navigate.svg") center/100% no-repeat; - mask: url("/images/navigate.svg") center/100% no-repeat; -} - -.actions__icon.--slow { - -webkit-mask: url("/images/slow.svg") center/100% no-repeat; - mask: url("/images/slow.svg") center/100% no-repeat; -} - -.actions__icon.--scroll { - -webkit-mask: url("/images/scroll.svg") center/100% no-repeat; - mask: url("/images/scroll.svg") center/100% no-repeat; -} - -.actions__icon.--error { - -webkit-mask: url("/images/error.svg") center/100% no-repeat; - mask: url("/images/error.svg") center/100% no-repeat; -} - -.actions__icon.--error { - -webkit-mask: url("/images/error.svg") center/100% no-repeat; - mask: url("/images/error.svg") center/100% no-repeat; -} - -.actions__icon.--protected { - -webkit-mask: url("/images/protected.svg") center/100% no-repeat; - mask: url("/images/protected.svg") center/100% no-repeat; -} - -.actions__icon.--modal { - -webkit-mask: url("/images/modal.svg") center/100% no-repeat; - mask: url("/images/modal.svg") center/100% no-repeat; -} - -.actions__icon.--native { - -webkit-mask: url("/images/native.svg") center/100% no-repeat; - mask: url("/images/native.svg") center/100% no-repeat; -} - -.actions__icon.--external { - -webkit-mask: url("/images/external.svg") center/100% no-repeat; - mask: url("/images/external.svg") center/100% no-repeat; -} - -.actions__icon.--redirect { - -webkit-mask: url("/images/redirect.svg") center/100% no-repeat; - mask: url("/images/redirect.svg") center/100% no-repeat; -} - -.actions__icon.--files { - -webkit-mask: url("/images/files.svg") center/100% no-repeat; - mask: url("/images/files.svg") center/100% no-repeat; -} - -.actions__icon.--reference { - -webkit-mask: url("/images/reference.svg") center/100% no-repeat; - mask: url("/images/reference.svg") center/100% no-repeat; -} - -.actions__icon.--turbo-drive { - -webkit-mask: url("/images/turbo-drive.svg") center/100% no-repeat; - mask: url("/images/turbo-drive.svg") center/100% no-repeat; -} - -.actions__icon.--turbo-frames { - -webkit-mask: url("/images/turbo-frames.svg") center/100% no-repeat; - mask: url("/images/turbo-frames.svg") center/100% no-repeat; -} - -.actions__icon.--turbo-streams { - -webkit-mask: url("/images/turbo-streams.svg") center/100% no-repeat; - mask: url("/images/turbo-streams.svg") center/100% no-repeat; -} - -.actions__icon.--turbo-native { - -webkit-mask: url("/images/turbo-native.svg") center/100% no-repeat; - mask: url("/images/turbo-native.svg") center/100% no-repeat; -} - -.auth { - background: var(--color-sheet); - padding: 0.75em; - border-radius: 0.5em; -} - -.auth__signout { - float: right; - display: inline-block; - width: auto; - padding: 0; - margin: 0; - background: none; - color: var(--color-text); -} - -.dialog { - display: none; - position: fixed; - z-index: 1; - padding-top: 100px; - left: 0; - top: 0; - width: 100%; - height: 100%; - overflow: auto; - background-color: rgb(0,0,0); - background-color: rgba(0,0,0,0.4); -} - -.dialog-content { - background-color: var(--color-sheet); - border-radius: 0.5em; - margin: auto; - padding: 20px; - border: 1px solid #888; - width: 80%; -} - -.dialog-close { - color: var(--color-text-subtle); - float: right; - font-size: 28px; - font-weight: bold; -} - -.dialog-close:hover, -.dialog-close:focus { - color: var(--color-text); - text-decoration: none; - cursor: pointer; -} - -.result { - color: var(--color-pink); - text-align: center; - padding-top: 20px; -} diff --git a/public/styles/base.css b/public/styles/base.css deleted file mode 100644 index 4be17e2..0000000 --- a/public/styles/base.css +++ /dev/null @@ -1,269 +0,0 @@ -:root { - --color-white: #fff; - --color-black: #191919; - --color-dark-gray: #262626; - --color-gray: #666; - --color-beige: #fbf7f0; - --color-yellow: #ffe801; - --color-teal: #5cd8e5; - --color-pink: #e9509a; - --color-blue: #317AF6; - --color-lilac: #C18BF4; - --color-text: var(--color-black); - --color-text-subtle: var(--color-gray); - --color-text-reversed: var(--color-white); - --color-tint: var(--color-lilac); - --color-tint-alt: var(--color-yellow); - --color-background-main: var(--color-white); - --color-background-index: var(--color-beige); - --color-sheet: var(--color-white); - --color-neutral: var(--color-beige); - --color-link: var(--color-tint); - --color-accent: var(--color-yellow); - --color-positive: var(--color-teal); - --color-negative: var(--color-pink); - --color-border: #DBDBDC; - --space-xs: 0.25em; - --space-s: 0.5em; - --space-m: 1em; - --space-l: 1.5em; - --space-xl: 3em; - --space-xxl: 4em; - --space-xxxl: 6em; - --font-main: -apple-system, "Helvetica Neue", helvetica, "Apple Color Emoji", arial, sans-serif; - --type-base: max(1.7em, min(calc(1em + 0.9vw), 2.4em)); - --type-xxxs: 55%; - --type-xxs: 65%; - --type-xs: 75%; - --type-s: 85%; - --type-m: 100%; - --type-l: 120%; - --type-xl: 150%; - --type-xxl: 200%; - --type-xxxl: 300%; - --type-xxxxl: 600%; - --leading-s: 1.1; - --leading-m: 1.4; - --leading-l:1.7 -} - -@media (prefers-color-scheme: dark) { - :root { - --color-pink: #ef7baa; - --color-text: var(--color-white); - --color-text-reversed: var(--color-black); - --color-neutral: var(--color-dark-gray); - --color-background-main: #000; - --color-background-index: #000; - --color-sheet: var(--color-dark-gray); - --color-border: #555; - } -} - -@media (min-width: 45em) { - :root { - --type-xxxl:390% - } -} - -*, :after, :before { - box-sizing:border-box -} - -::-moz-selection, ::selection { - color: var(--color-sheet); - background-color:var(--color-text) -} - -html { - font-size: 10px; - background-color: var(--color-background-main); - -webkit-tap-highlight-color: transparent; -} - -body { - display: flex; - flex-direction: column; - align-items: stretch; - min-height: 100%; - min-height: 100dvh; - overflow-x: hidden; - margin: 0; - padding: 1em 0 0; - font-family: var(--font-main); - font-size: var(--type-base); - /* font-variation-settings: "wdth" 180; */ - color: var(--color-text); - background-color: transparent; -} - -@media (min-width: 45em) { - body { padding-top: 2em; } -} - -h1, h2, h3, h4, h5, h6 { - line-height:var(--leading-m) -} - -h1 { - margin: 0 0 var(--space-xs) 0; - font-size: var(--type-xxl); - font-weight: 700; - letter-spacing: -.02em; - line-height: var(--leading-s); - transition:font-weight .2s ease -} - -h2 { - margin: var(--space-l) 0 var(--space-s) 0; - font-size: var(--type-l); - font-weight: 600; - font-style:italic -} - -h3 { - margin: var(--space-l) 0 var(--space-xs) 0; - font-size: var(--type-m); - font-weight:900 -} - -h4, h5, h6 { - margin: var(--space-m) 0 0 0; - font-weight:600 -} - -dl, h4, h5, h6, ol, p, ul { - font-size:var(--type-m) -} - -dl, ol, p, ul { - margin: 0 0 var(--space-m); - line-height: var(--leading-l); - font-weight:350 -} - -code { - padding: .2em .3em; - color: var(--color-text); - background-color: var(--color-neutral); - border-radius: 0.2em; - font-size: 0.95em; -} - -blockquote { - position: relative; - margin: var(--space-l) 0; - padding: 0; - quotes: "\201C""\201D""\2018""\2019"; - text-transform: uppercase; - font-size: var(--type-m); - letter-spacing: .1em; - font-weight: 200; - font-style:italic -} - -ol, ul { - padding: 0; - list-style-position:outside -} - -ul { - list-style-type:none -} - -ol { - list-style-type:decimal -} - -ol li, ul li { - margin-left: 0; - margin-bottom: var(--space-xs); - position:relative -} - -ol li ol, ol li ul, ul li ol, ul li ul { - margin-left: 1.25em; - margin-bottom: 0; - font-size:var(--type-m) -} - -b, strong { - font-weight:600 -} - -em, i { - font-style:italic -} - -small { - font-size:var(--type-s) -} - -a, a:visited { - color: var(--color-text); - text-decoration-thickness: .1em; - text-decoration-width: .1rem; - font-weight: 600; -} - -a:hover { - -webkit-text-decoration-color: var(--color-yellow); - text-decoration-color: var(--color-yellow); - text-decoration-thickness: .2em; - text-decoration-width:.2em -} - -embed, img, object, video { - max-width: 100%; - height:auto -} - -hr { - width: 100%; - margin: var(--space-xl) 0; - border: 0; - border-top:.1rem solid var(--color-text) -} - -input[type=text] { - display: block; - width: 100%; - padding: 0.75em; - margin: 0 0 var(--space-m); - font-size: var(--type-m); - color: var(--color-text); - background: var(--color-sheet); - border: 1px solid var(--color-border); -} - -button, -.button, -.button:visited { - display: block; - width: 100%; - padding: 0.75em 1.5em; - margin: 0 0 var(--space-m); - background: var(--color-tint); - font-size: var(--type-m); - font-weight: 500; - color: var(--color-white); - text-align: center; - line-height: 1.25; - border: 0; - border-radius: 0.5em; - text-decoration: none; -} - -.button:active { - background: var(--color-tint-alt); -} - -.button:disabled { - background: var(--color-gray); -} - -@media (min-width: 45em) { - button { - width: auto; - } -} diff --git a/public/styles/bridge.css b/public/styles/bridge.css deleted file mode 100644 index 51f6f68..0000000 --- a/public/styles/bridge.css +++ /dev/null @@ -1,18 +0,0 @@ -/* Bridge Components — hide elements for registered bridge components */ - -/* - * Hide the submit button when the "form" component is registered. - */ -[data-bridge-components~="form"] -[data-controller~="bridge--form"] -[type="submit"] { - display: none; -} - -/* - * Hide the overflow button when the "overflow-menu" component is registered. - */ -[data-bridge-components~="overflow-menu"] -[data-controller~="bridge--overflow-menu"] { - display: none; -} diff --git a/public/styles/native.css b/public/styles/native.css deleted file mode 100644 index 96100c8..0000000 --- a/public/styles/native.css +++ /dev/null @@ -1,37 +0,0 @@ -a, -a:visited { - font-weight: inherit; -} - -.display.--none\@native { - display: none; -} - -.page-title:not(.display-native) { - display: none; -} - -input[type=text]:focus { - outline: 0; -} - -.actions__item { - text-decoration: none; -} - -.button\@native, -.button\@native:visited { - display: block; - width: 100%; - padding: 0.75em 1.5em; - margin: 0 0 var(--space-m); - background: var(--color-tint); - font-size: var(--type-m); - font-weight: 500; - color: var(--color-white); - text-align: center; - text-decoration: none; - line-height: 1.25; - border: 0; - border-radius: 0.5em; -} diff --git a/public/styles/utilities.css b/public/styles/utilities.css deleted file mode 100644 index d40c012..0000000 --- a/public/styles/utilities.css +++ /dev/null @@ -1,455 +0,0 @@ -.display.--block { - display:block -} - -.display.--inline { - display:inline -} - -.display.--none { - display:none -} - -@media (max-width: 44.9em) { - .display.--none-s { - display:none - } -} - -.display.--sr-only { - clip: rect(1px, 1px, 1px, 1px); - position: absolute !important; - height: 1px; - width: 1px; - overflow:hidden -} - -.grid { - display: grid; - grid-template-columns: 1fr minmax(80vw, 100%) 1fr; - align-items: start; - grid-column-gap: min(5vw, 4rem); - column-gap:min(5vw, 4rem) -} - -.grid > .grid { - grid-column:1/-1; -} - -.grid > :not([class*=grid]) { - grid-column:2; -} - -.grid.--dense { - grid-auto-flow:dense -} - -@media (min-width: 45em) { - .grid { - grid-template-columns: 1fr repeat(12, minmax(2rem, 100%)) 1fr; - grid-column-gap: min(2.5vw, 4rem); - column-gap:min(2.5vw, 4rem) - } - - .grid > :not([class*=grid]) { - grid-column:4/span 8 - } -} - -@media (min-width: 90em) { - .grid { - grid-template-columns: 1fr repeat(12, 7.5rem) 1fr; - grid-column-gap: 4rem; - column-gap:4rem - } -} - -.grid__item { - grid-column-start:2 -} - -.grid__item.--bleed-left { - grid-column:1/span 2 -} - -.grid__item.--bleed-right { - grid-column-end:-1 -} - -.grid__item.--bleed-full { - grid-column:1/-1 -} - -@media (min-width: 45em) { - .grid__item { - grid-column:4/span 8 - } - - .grid__item.--span-3 { - grid-column-end:span 3 - } - - .grid__item.--span-4 { - grid-column-end:span 4 - } - - .grid__item.--span-5 { - grid-column-end:span 5 - } - - .grid__item.--span-6 { - grid-column-end:span 6 - } - - .grid__item.--span-7 { - grid-column-end:span 7 - } - - .grid__item.--span-8 { - grid-column-end:span 8 - } - - .grid__item.--span-9 { - grid-column-end:span 9 - } - - .grid__item.--span-10 { - grid-column-end:span 10 - } - - .grid__item.--span-11 { - grid-column-end:span 11 - } - - .grid__item.--span-12 { - grid-column:2/span 12 - } - - .grid__item.--start-1 { - grid-column-start:2 - } - - .grid__item.--start-2 { - grid-column-start:3 - } - - .grid__item.--start-3 { - grid-column-start:4 - } - - .grid__item.--start-4 { - grid-column-start:5 - } - - .grid__item.--start-5 { - grid-column-start:6 - } - - .grid__item.--start-6 { - grid-column-start:7 - } - - .grid__item.--start-7 { - grid-column-start:8 - } - - .grid__item.--start-8 { - grid-column-start:9 - } - - .grid__item.--start-9 { - grid-column-start:10 - } - - .grid__item.--start-10 { - grid-column-start:11 - } - - .grid__item.--start-auto { - grid-column-start:auto - } - - .grid__item.--place-start { - align-self: start; - justify-self: start; - place-self:start - } - - .grid__item.--place-center { - align-self: center; - justify-self: center; - place-self:center - } - - .grid__item.--place-end { - align-self: end; - justify-self: end; - place-self:end - } -} - -.pad.--hard { - padding:var(--space-m) -} - -.pad.--firm { - padding:var(--space-l) -} - -.pad.--soft { - padding:var(--space-xl) -} - -.pad.--top-hard { - padding-top:var(--space-m) -} - -.pad.--top-firm { - padding-top:var(--space-l) -} - -.pad.--top-soft { - padding-top:var(--space-xl) -} - -.pad.--bottom-hard { - padding-bottom:var(--space-m) -} - -.pad.--bottom-firm { - padding-bottom:var(--space-l) -} - -.pad.--bottom-soft { - padding-bottom:var(--space-xl) -} - -.pad.--left-hard { - padding-left:var(--space-m) -} - -.pad.--left-firm { - padding-left:var(--space-l) -} - -.pad.--left-soft { - padding-left:var(--space-xl) -} - -.pad.--right-hard { - padding-right:var(--space-m) -} - -.pad.--right-firm { - padding-right:var(--space-l) -} - -.pad.--right-soft { - padding-right:var(--space-xl) -} - -.space.--top-flush { - margin-top:0 -} - -@media (min-width: 45em) { - .space.--top-flush-soft { - margin-top:0 - } -} - -.space.--top-s { - margin-top:var(--space-s) -} - -.space.--top-m { - margin-top:var(--space-m) -} - -.space.--top-l { - margin-top:var(--space-l) -} - -.space.--top-xl { - margin-top:var(--space-xl) -} - -.space.--top-xxl { - margin-top:var(--space-xxl) -} - -.space.--top-pull-s { - margin-top:calc(var(--space-s) * -1) -} - -.space.--top-pull-m { - margin-top:calc(var(--space-m) * -1) -} - -.space.--top-pull-l { - margin-top:calc(var(--space-l) * -1) -} - -.space.--top-pull-xl { - margin-top:calc(var(--space-xl) * -1) -} - -.space.--top-pull-xxl { - margin-top:calc(var(--space-xxl) * -1) -} - -.space.--bottom-flush { - margin-bottom:0 -} - -.space.--ends-flush { - margin-top: 0; - margin-bottom: 0; -} - -@media (min-width: 45em) { - .space.--bottom-flush-soft { - margin-bottom:0 - } -} - -.space.--bottom-s { - margin-bottom:var(--space-s) -} - -.space.--bottom-m { - margin-bottom:var(--space-m) -} - -.space.--bottom-l { - margin-bottom:var(--space-l) -} - -.space.--bottom-xl { - margin-bottom:var(--space-xl) -} - -.space.--bottom-xxl { - margin-bottom:var(--space-xxl) -} - -.space.--bottom-pull-s { - margin-bottom:calc(var(--space-s) * -1) -} - -.space.--bottom-pull-m { - margin-bottom:calc(var(--space-m) * -1) -} - -.space.--bottom-pull-l { - margin-bottom:calc(var(--space-l) * -1) -} - -.space.--bottom-pull-xl { - margin-bottom:calc(var(--space-xl) * -1) -} - -.space.--bottom-pull-xxl { - margin-bottom:calc(var(--space-xxl) * -1) -} - -.text.--size-xxxs { - font-size:var(--type-xxxs) -} - -.text.--size-xxs { - font-size:var(--type-xxs) -} - -.text.--size-xs { - font-size:var(--type-xs) -} - -.text.--size-s { - font-size:var(--type-s) -} - -.text.--size-m { - font-size:var(--type-m) -} - -.text.--size-l { - font-size:var(--type-l) -} - -.text.--size-xl { - font-size: var(--type-xl); - line-height:var(--leading-m) -} - -.text.--size-xxl { - font-size: var(--type-xxl); - line-height:var(--leading-m) -} - -.text.--size-xxxl { - font-size: var(--type-xxxl); - line-height:var(--leading-s) -} - -.text.--size-xxxxl { - font-size: var(--type-xxxxl); - line-height: var(--leading-s); - letter-spacing:-.02em -} - -.text.--weight-200 { - font-weight:200 -} - -.text.--weight-300 { - font-weight:300 -} - -.text.--weight-400 { - font-weight:400 -} - -.text.--weight-500 { - font-weight:500 -} - -.text.--weight-600 { - font-weight:600 -} - -.text.--weight-700 { - font-weight:700 -} - -.text.--weight-800 { - font-weight:800 -} - -.text.--align-left { - text-align:left -} - -.text.--align-center { - text-align:center -} - -.text.--align-right { - text-align:right -} - -.text.--leading-s { - line-height:var(--leading-s) -} - -.text.--leading-m { - line-height:var(--leading-m) -} - -.text.--leading-l { - line-height:var(--leading-l) -} - -.text.--color-subtle { - color: var(--color-text-subtle); -} \ No newline at end of file diff --git a/script/.keep b/script/.keep new file mode 100644 index 0000000..e69de29 diff --git a/server.js b/server.js deleted file mode 100644 index 298c9cd..0000000 --- a/server.js +++ /dev/null @@ -1,181 +0,0 @@ -import express from "express" -import layouts from "express-ejs-layouts" -import cookieParser from "cookie-parser" -import multer from "multer" -const upload = multer() -const app = express(); - -// Ensure we use environment port if available for deploying -const PORT = process.env.PORT || 45678 - -app.set("view engine", "ejs") -app.use(express.static("public")) -app.use(express.static("public/javascript")) -app.use(express.static("json")) -app.use(cookieParser()) -app.use(layouts) - -// Determine platform -app.use((request, response, next) => { - const userAgent = request.get("User-Agent") - response.locals.native_app = userAgent.includes("Turbo Native") - next() -}) - -// Auth -app.use((request, response, next) => { - response.locals.authenticated = request.cookies && request.cookies.authenticated - next() -}) - -// Logging - -app.use((request, response, next) => { - console.log(`${Date()} -- ${request.method} ${request.path}`) - next() -}) - -// JSON - -app.use((request, response, next) => { - if (request.url.endsWith(".json")) { - res.type('json') - } - - next() -}) - -// Routes - -app.get("/", (request, response) => { - response.render("index", { title: "Hotwire Native Demo", page_class: "index" }) -}) - -app.get("/one", (request, response) => { - response.render("one", { title: "How’d You Get Here?" }) -}) - -app.get("/two", (request, response) => { - response.render("two", { title: "Push or Replace?", action: request.query.action }) -}) - -app.get("/long", (request, response) => { - response.render("long", { title: "A Really Long Page" }) -}) - -app.get("/scroll", (request, response) => { - response.render("scroll", { title: "Restoring Your Scroll" }) -}) - -app.get("/follow", (request, response) => { - response.redirect("/redirected") -}) - -app.get("/redirected", (request, response) => { - response.render("redirected", { title: "Redirected Page" }) -}) - -app.get("/follow-external-redirect", (request, response) => { - response.redirect("https://37signals.com") -}) - -app.get("/reference", (request, response) => { - response.render("reference", { title: "Reference", page_class: "index" }) -}) - -app.get("/files", (request, response) => { - response.render("files", { title: "Handling Files" }) -}) - -app.get("/new", (request, response) => { - response.render("new", { title: "A Modal Webpage" }) -}) - -app.post("/new", (request, response) => { - response.redirect("/success") -}) - -app.get("/bridge-form", (request, response) => { - response.render("bridge-form", { title: "Bridge Form" }) -}) - -app.post("/bridge-form", (request, response) => { - setTimeout(() => { - response.redirect("/success") - }, 1500) -}) - -app.get("/bridge-menu", (request, response) => { - response.render("bridge-menu", { title: "Bridge Menu" }) -}) - -app.get("/bridge-overflow", (request, response) => { - response.render("bridge-overflow", { title: "Bridge Overflow" }) -}) - -app.get("/success", (request, response) => { - response.render("success", { title: "It Worked!" }) -}) - -app.get("/numbers", (request, response) => { - response.render("numbers", { title: "A List of Numbers" }) -}) - -app.get("/nonexistent", (request, response) => { - response.status(404).send("Not Found") -}) - -app.get("/reference/turbo-drive", (request, response) => { - response.render("turbo-drive", { title: "Turbo Drive" }) -}) - -app.get("/reference/turbo-frames", (request, response) => { - response.render("turbo-frames", { title: "Turbo Frames" }) -}) - -app.get("/reference/turbo-streams", (request, response) => { - response.render("turbo-streams", { title: "Turbo Streams" }) -}) - -app.get("/reference/turbo-native", (request, response) => { - response.render("turbo-native", { title: "Turbo Native" }) -}) - -app.get("/protected", (request, response) => { - if (response.locals.authenticated) { - response.render("protected", { title: "Protected Webpage" }) - } else { - response.status(401).send("Unauthorized") - } -}) - -app.get("/signin", (request, response) => { - response.render("signin", { title: "Sign In" }) -}) - -app.post("/signin", upload.none(), (request, response) => { - // Cookie expires in one day - const expiration = new Date(Date.now() + 86400000) - - response.cookie("authenticated", request.body.name, { expires: expiration, httpOnly: true }) - response.redirect("/") -}) - -app.post("/signout", (request, response) => { - response.clearCookie("authenticated") - response.redirect("/") -}) - -app.get("/slow", (request, response) => { - setTimeout(() => { - response.render("slow", { title: "Slow-loading Page" }) - }, 3000) -}) - -app.get("/test", (request, response) => { - response.sendStatus(200) -}) - -const listener = app.listen(PORT, () => { - console.log("Server is listening on port " + listener.address().port); -}) diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 0000000..e69de29 diff --git a/tmp/storage/.keep b/tmp/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/javascript/@hotwired--hotwire-native-bridge.js b/vendor/javascript/@hotwired--hotwire-native-bridge.js new file mode 100644 index 0000000..fc01cd8 --- /dev/null +++ b/vendor/javascript/@hotwired--hotwire-native-bridge.js @@ -0,0 +1,4 @@ +// @hotwired/hotwire-native-bridge@1.0.0 downloaded from https://ga.jspm.io/npm:@hotwired/hotwire-native-bridge@1.0.0/dist/hotwire-native-bridge.js + +import{Controller as e}from"@hotwired/stimulus";var t=Object.defineProperty;var __defNormalProp=(e,s,n)=>s in e?t(e,s,{enumerable:true,configurable:true,writable:true,value:n}):e[s]=n;var __publicField=(e,t,s)=>{__defNormalProp(e,typeof t!=="symbol"?t+"":t,s);return s};var s=class{#e;#t;#s;#n;constructor(){this.#e=null;this.#t=0;this.#s=[];this.#n=new Map}start(){this.notifyApplicationAfterStart()}notifyApplicationAfterStart(){document.dispatchEvent(new Event("web-bridge:ready"))}supportsComponent(e){return!!this.#e&&this.#e.supportsComponent(e)}send({component:e,event:t,data:s,callback:n}){if(!this.#e){this.#a({component:e,event:t,data:s,callback:n});return null}if(!this.supportsComponent(e))return null;const a=this.generateMessageId();const i={id:a,component:e,event:t,data:s||{}};this.#e.receive(i);n&&this.#n.set(a,n);return a}receive(e){this.executeCallbackFor(e)}executeCallbackFor(e){const t=this.#n.get(e.id);t&&t(e)}removeCallbackFor(e){this.#n.has(e)&&this.#n.delete(e)}removePendingMessagesFor(e){this.#s=this.#s.filter((t=>t.component!=e))}generateMessageId(){const e=++this.#t;return e.toString()}setAdapter(e){this.#e=e;document.documentElement.dataset.bridgePlatform=this.#e.platform;this.adapterDidUpdateSupportedComponents();this.#i()}adapterDidUpdateSupportedComponents(){this.#e&&(document.documentElement.dataset.bridgeComponents=this.#e.supportedComponents.join(" "))}#a(e){this.#s.push(e)}#i(){this.#s.forEach((e=>this.send(e)));this.#s=[]}};var n=class{constructor(e){this.element=e}get title(){return(this.bridgeAttribute("title")||this.attribute("aria-label")||this.element.textContent||this.element.value).trim()}get enabled(){return!this.disabled}get disabled(){const e=this.bridgeAttribute("disabled");return e==="true"||e===this.platform}enableForComponent(e){e.enabled&&this.removeBridgeAttribute("disabled")}hasClass(e){return this.element.classList.contains(e)}attribute(e){return this.element.getAttribute(e)}bridgeAttribute(e){return this.attribute(`data-bridge-${e}`)}setBridgeAttribute(e,t){this.element.setAttribute(`data-bridge-${e}`,t)}removeBridgeAttribute(e){this.element.removeAttribute(`data-bridge-${e}`)}click(){this.platform=="android"&&this.element.removeAttribute("target");this.element.click()}get platform(){return document.documentElement.dataset.bridgePlatform}};var{userAgent:a}=window.navigator;var i=/bridge-components: \[.+\]/.test(a);var r=class extends e{static get shouldLoad(){return i}pendingMessageCallbacks=[];initialize(){this.pendingMessageCallbacks=[]}connect(){}disconnect(){this.removePendingCallbacks();this.removePendingMessages()}get component(){return this.constructor.component}get platformOptingOut(){const{bridgePlatform:e}=document.documentElement.dataset;return this.identifier==this.element.getAttribute(`data-controller-optout-${e}`)}get enabled(){return!this.platformOptingOut&&this.bridge.supportsComponent(this.component)}send(e,t={},s){t.metadata={url:window.location.href};const n={component:this.component,event:e,data:t,callback:s};const a=this.bridge.send(n);s&&this.pendingMessageCallbacks.push(a)}removePendingCallbacks(){this.pendingMessageCallbacks.forEach((e=>this.bridge.removeCallbackFor(e)))}removePendingMessages(){this.bridge.removePendingMessagesFor(this.component)}get bridgeElement(){return new n(this.element)}get bridge(){return window.Strada.web}};__publicField(r,"component","");if(!window.Strada){const e=new s;window.Strada={web:e};e.start()}export{r as BridgeComponent,n as BridgeElement}; + diff --git a/vendor/javascript/@hotwired--stimulus.js b/vendor/javascript/@hotwired--stimulus.js new file mode 100644 index 0000000..07dd4a6 --- /dev/null +++ b/vendor/javascript/@hotwired--stimulus.js @@ -0,0 +1,4 @@ +// @hotwired/stimulus@3.2.2 downloaded from https://ga.jspm.io/npm:@hotwired/stimulus@3.2.2/dist/stimulus.js + +class EventListener{constructor(e,t,r){this.eventTarget=e;this.eventName=t;this.eventOptions=r;this.unorderedBindings=new Set}connect(){this.eventTarget.addEventListener(this.eventName,this,this.eventOptions)}disconnect(){this.eventTarget.removeEventListener(this.eventName,this,this.eventOptions)}bindingConnected(e){this.unorderedBindings.add(e)}bindingDisconnected(e){this.unorderedBindings.delete(e)}handleEvent(e){const t=extendEvent(e);for(const e of this.bindings){if(t.immediatePropagationStopped)break;e.handleEvent(t)}}hasBindings(){return this.unorderedBindings.size>0}get bindings(){return Array.from(this.unorderedBindings).sort(((e,t)=>{const r=e.index,s=t.index;return rs?1:0}))}}function extendEvent(e){if("immediatePropagationStopped"in e)return e;{const{stopImmediatePropagation:t}=e;return Object.assign(e,{immediatePropagationStopped:false,stopImmediatePropagation(){this.immediatePropagationStopped=true;t.call(this)}})}}class Dispatcher{constructor(e){this.application=e;this.eventListenerMaps=new Map;this.started=false}start(){if(!this.started){this.started=true;this.eventListeners.forEach((e=>e.connect()))}}stop(){if(this.started){this.started=false;this.eventListeners.forEach((e=>e.disconnect()))}}get eventListeners(){return Array.from(this.eventListenerMaps.values()).reduce(((e,t)=>e.concat(Array.from(t.values()))),[])}bindingConnected(e){this.fetchEventListenerForBinding(e).bindingConnected(e)}bindingDisconnected(e,t=false){this.fetchEventListenerForBinding(e).bindingDisconnected(e);t&&this.clearEventListenersForBinding(e)}handleError(e,t,r={}){this.application.handleError(e,`Error ${t}`,r)}clearEventListenersForBinding(e){const t=this.fetchEventListenerForBinding(e);if(!t.hasBindings()){t.disconnect();this.removeMappedEventListenerFor(e)}}removeMappedEventListenerFor(e){const{eventTarget:t,eventName:r,eventOptions:s}=e;const n=this.fetchEventListenerMapForEventTarget(t);const i=this.cacheKey(r,s);n.delete(i);0==n.size&&this.eventListenerMaps.delete(t)}fetchEventListenerForBinding(e){const{eventTarget:t,eventName:r,eventOptions:s}=e;return this.fetchEventListener(t,r,s)}fetchEventListener(e,t,r){const s=this.fetchEventListenerMapForEventTarget(e);const n=this.cacheKey(t,r);let i=s.get(n);if(!i){i=this.createEventListener(e,t,r);s.set(n,i)}return i}createEventListener(e,t,r){const s=new EventListener(e,t,r);this.started&&s.connect();return s}fetchEventListenerMapForEventTarget(e){let t=this.eventListenerMaps.get(e);if(!t){t=new Map;this.eventListenerMaps.set(e,t)}return t}cacheKey(e,t){const r=[e];Object.keys(t).sort().forEach((e=>{r.push(`${t[e]?"":"!"}${e}`)}));return r.join(":")}}const e={stop({event:e,value:t}){t&&e.stopPropagation();return true},prevent({event:e,value:t}){t&&e.preventDefault();return true},self({event:e,value:t,element:r}){return!t||r===e.target}};const t=/^(?:(?:([^.]+?)\+)?(.+?)(?:\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/;function parseActionDescriptorString(e){const r=e.trim();const s=r.match(t)||[];let n=s[2];let i=s[3];if(i&&!["keydown","keyup","keypress"].includes(n)){n+=`.${i}`;i=""}return{eventTarget:parseEventTarget(s[4]),eventName:n,eventOptions:s[7]?parseEventOptions(s[7]):{},identifier:s[5],methodName:s[6],keyFilter:s[1]||i}}function parseEventTarget(e){return"window"==e?window:"document"==e?document:void 0}function parseEventOptions(e){return e.split(":").reduce(((e,t)=>Object.assign(e,{[t.replace(/^!/,"")]:!/^!/.test(t)})),{})}function stringifyEventTarget(e){return e==window?"window":e==document?"document":void 0}function camelize(e){return e.replace(/(?:[_-])([a-z0-9])/g,((e,t)=>t.toUpperCase()))}function namespaceCamelize(e){return camelize(e.replace(/--/g,"-").replace(/__/g,"_"))}function capitalize(e){return e.charAt(0).toUpperCase()+e.slice(1)}function dasherize(e){return e.replace(/([A-Z])/g,((e,t)=>`-${t.toLowerCase()}`))}function tokenize(e){return e.match(/[^\s]+/g)||[]}function isSomething(e){return null!==e&&void 0!==e}function hasProperty(e,t){return Object.prototype.hasOwnProperty.call(e,t)}const r=["meta","ctrl","alt","shift"];class Action{constructor(e,t,r,s){this.element=e;this.index=t;this.eventTarget=r.eventTarget||e;this.eventName=r.eventName||getDefaultEventNameForElement(e)||error("missing event name");this.eventOptions=r.eventOptions||{};this.identifier=r.identifier||error("missing identifier");this.methodName=r.methodName||error("missing method name");this.keyFilter=r.keyFilter||"";this.schema=s}static forToken(e,t){return new this(e.element,e.index,parseActionDescriptorString(e.content),t)}toString(){const e=this.keyFilter?`.${this.keyFilter}`:"";const t=this.eventTargetName?`@${this.eventTargetName}`:"";return`${this.eventName}${e}${t}->${this.identifier}#${this.methodName}`}shouldIgnoreKeyboardEvent(e){if(!this.keyFilter)return false;const t=this.keyFilter.split("+");if(this.keyFilterDissatisfied(e,t))return true;const s=t.filter((e=>!r.includes(e)))[0];if(!s)return false;hasProperty(this.keyMappings,s)||error(`contains unknown key filter: ${this.keyFilter}`);return this.keyMappings[s].toLowerCase()!==e.key.toLowerCase()}shouldIgnoreMouseEvent(e){if(!this.keyFilter)return false;const t=[this.keyFilter];return!!this.keyFilterDissatisfied(e,t)}get params(){const e={};const t=new RegExp(`^data-${this.identifier}-(.+)-param$`,"i");for(const{name:r,value:s}of Array.from(this.element.attributes)){const n=r.match(t);const i=n&&n[1];i&&(e[camelize(i)]=typecast(s))}return e}get eventTargetName(){return stringifyEventTarget(this.eventTarget)}get keyMappings(){return this.schema.keyMappings}keyFilterDissatisfied(e,t){const[s,n,i,o]=r.map((e=>t.includes(e)));return e.metaKey!==s||e.ctrlKey!==n||e.altKey!==i||e.shiftKey!==o}}const s={a:()=>"click",button:()=>"click",form:()=>"submit",details:()=>"toggle",input:e=>"submit"==e.getAttribute("type")?"click":"input",select:()=>"change",textarea:()=>"input"};function getDefaultEventNameForElement(e){const t=e.tagName.toLowerCase();if(t in s)return s[t](e)}function error(e){throw new Error(e)}function typecast(e){try{return JSON.parse(e)}catch(t){return e}}class Binding{constructor(e,t){this.context=e;this.action=t}get index(){return this.action.index}get eventTarget(){return this.action.eventTarget}get eventOptions(){return this.action.eventOptions}get identifier(){return this.context.identifier}handleEvent(e){const t=this.prepareActionEvent(e);this.willBeInvokedByEvent(e)&&this.applyEventModifiers(t)&&this.invokeWithEvent(t)}get eventName(){return this.action.eventName}get method(){const e=this.controller[this.methodName];if("function"==typeof e)return e;throw new Error(`Action "${this.action}" references undefined method "${this.methodName}"`)}applyEventModifiers(e){const{element:t}=this.action;const{actionDescriptorFilters:r}=this.context.application;const{controller:s}=this.context;let n=true;for(const[i,o]of Object.entries(this.eventOptions))if(i in r){const c=r[i];n=n&&c({name:i,value:o,event:e,element:t,controller:s})}return n}prepareActionEvent(e){return Object.assign(e,{params:this.action.params})}invokeWithEvent(e){const{target:t,currentTarget:r}=e;try{this.method.call(this.controller,e);this.context.logDebugActivity(this.methodName,{event:e,target:t,currentTarget:r,action:this.methodName})}catch(t){const{identifier:r,controller:s,element:n,index:i}=this;const o={identifier:r,controller:s,element:n,index:i,event:e};this.context.handleError(t,`invoking action "${this.action}"`,o)}}willBeInvokedByEvent(e){const t=e.target;return!(e instanceof KeyboardEvent&&this.action.shouldIgnoreKeyboardEvent(e))&&(!(e instanceof MouseEvent&&this.action.shouldIgnoreMouseEvent(e))&&(this.element===t||(t instanceof Element&&this.element.contains(t)?this.scope.containsElement(t):this.scope.containsElement(this.action.element))))}get controller(){return this.context.controller}get methodName(){return this.action.methodName}get element(){return this.scope.element}get scope(){return this.context.scope}}class ElementObserver{constructor(e,t){this.mutationObserverInit={attributes:true,childList:true,subtree:true};this.element=e;this.started=false;this.delegate=t;this.elements=new Set;this.mutationObserver=new MutationObserver((e=>this.processMutations(e)))}start(){if(!this.started){this.started=true;this.mutationObserver.observe(this.element,this.mutationObserverInit);this.refresh()}}pause(e){if(this.started){this.mutationObserver.disconnect();this.started=false}e();if(!this.started){this.mutationObserver.observe(this.element,this.mutationObserverInit);this.started=true}}stop(){if(this.started){this.mutationObserver.takeRecords();this.mutationObserver.disconnect();this.started=false}}refresh(){if(this.started){const e=new Set(this.matchElementsInTree());for(const t of Array.from(this.elements))e.has(t)||this.removeElement(t);for(const t of Array.from(e))this.addElement(t)}}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){if("attributes"==e.type)this.processAttributeChange(e.target,e.attributeName);else if("childList"==e.type){this.processRemovedNodes(e.removedNodes);this.processAddedNodes(e.addedNodes)}}processAttributeChange(e,t){this.elements.has(e)?this.delegate.elementAttributeChanged&&this.matchElement(e)?this.delegate.elementAttributeChanged(e,t):this.removeElement(e):this.matchElement(e)&&this.addElement(e)}processRemovedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.processTree(e,this.removeElement)}}processAddedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.elementIsActive(e)&&this.processTree(e,this.addElement)}}matchElement(e){return this.delegate.matchElement(e)}matchElementsInTree(e=this.element){return this.delegate.matchElementsInTree(e)}processTree(e,t){for(const r of this.matchElementsInTree(e))t.call(this,r)}elementFromNode(e){if(e.nodeType==Node.ELEMENT_NODE)return e}elementIsActive(e){return e.isConnected==this.element.isConnected&&this.element.contains(e)}addElement(e){if(!this.elements.has(e)&&this.elementIsActive(e)){this.elements.add(e);this.delegate.elementMatched&&this.delegate.elementMatched(e)}}removeElement(e){if(this.elements.has(e)){this.elements.delete(e);this.delegate.elementUnmatched&&this.delegate.elementUnmatched(e)}}}class AttributeObserver{constructor(e,t,r){this.attributeName=t;this.delegate=r;this.elementObserver=new ElementObserver(e,this)}get element(){return this.elementObserver.element}get selector(){return`[${this.attributeName}]`}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get started(){return this.elementObserver.started}matchElement(e){return e.hasAttribute(this.attributeName)}matchElementsInTree(e){const t=this.matchElement(e)?[e]:[];const r=Array.from(e.querySelectorAll(this.selector));return t.concat(r)}elementMatched(e){this.delegate.elementMatchedAttribute&&this.delegate.elementMatchedAttribute(e,this.attributeName)}elementUnmatched(e){this.delegate.elementUnmatchedAttribute&&this.delegate.elementUnmatchedAttribute(e,this.attributeName)}elementAttributeChanged(e,t){this.delegate.elementAttributeValueChanged&&this.attributeName==t&&this.delegate.elementAttributeValueChanged(e,t)}}function add(e,t,r){fetch(e,t).add(r)}function del(e,t,r){fetch(e,t).delete(r);prune(e,t)}function fetch(e,t){let r=e.get(t);if(!r){r=new Set;e.set(t,r)}return r}function prune(e,t){const r=e.get(t);null!=r&&0==r.size&&e.delete(t)}class Multimap{constructor(){this.valuesByKey=new Map}get keys(){return Array.from(this.valuesByKey.keys())}get values(){const e=Array.from(this.valuesByKey.values());return e.reduce(((e,t)=>e.concat(Array.from(t))),[])}get size(){const e=Array.from(this.valuesByKey.values());return e.reduce(((e,t)=>e+t.size),0)}add(e,t){add(this.valuesByKey,e,t)}delete(e,t){del(this.valuesByKey,e,t)}has(e,t){const r=this.valuesByKey.get(e);return null!=r&&r.has(t)}hasKey(e){return this.valuesByKey.has(e)}hasValue(e){const t=Array.from(this.valuesByKey.values());return t.some((t=>t.has(e)))}getValuesForKey(e){const t=this.valuesByKey.get(e);return t?Array.from(t):[]}getKeysForValue(e){return Array.from(this.valuesByKey).filter((([t,r])=>r.has(e))).map((([e,t])=>e))}}class IndexedMultimap extends Multimap{constructor(){super();this.keysByValue=new Map}get values(){return Array.from(this.keysByValue.keys())}add(e,t){super.add(e,t);add(this.keysByValue,t,e)}delete(e,t){super.delete(e,t);del(this.keysByValue,t,e)}hasValue(e){return this.keysByValue.has(e)}getKeysForValue(e){const t=this.keysByValue.get(e);return t?Array.from(t):[]}}class SelectorObserver{constructor(e,t,r,s){this._selector=t;this.details=s;this.elementObserver=new ElementObserver(e,this);this.delegate=r;this.matchesByElement=new Multimap}get started(){return this.elementObserver.started}get selector(){return this._selector}set selector(e){this._selector=e;this.refresh()}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get element(){return this.elementObserver.element}matchElement(e){const{selector:t}=this;if(t){const r=e.matches(t);return this.delegate.selectorMatchElement?r&&this.delegate.selectorMatchElement(e,this.details):r}return false}matchElementsInTree(e){const{selector:t}=this;if(t){const r=this.matchElement(e)?[e]:[];const s=Array.from(e.querySelectorAll(t)).filter((e=>this.matchElement(e)));return r.concat(s)}return[]}elementMatched(e){const{selector:t}=this;t&&this.selectorMatched(e,t)}elementUnmatched(e){const t=this.matchesByElement.getKeysForValue(e);for(const r of t)this.selectorUnmatched(e,r)}elementAttributeChanged(e,t){const{selector:r}=this;if(r){const t=this.matchElement(e);const s=this.matchesByElement.has(r,e);t&&!s?this.selectorMatched(e,r):!t&&s&&this.selectorUnmatched(e,r)}}selectorMatched(e,t){this.delegate.selectorMatched(e,t,this.details);this.matchesByElement.add(t,e)}selectorUnmatched(e,t){this.delegate.selectorUnmatched(e,t,this.details);this.matchesByElement.delete(t,e)}}class StringMapObserver{constructor(e,t){this.element=e;this.delegate=t;this.started=false;this.stringMap=new Map;this.mutationObserver=new MutationObserver((e=>this.processMutations(e)))}start(){if(!this.started){this.started=true;this.mutationObserver.observe(this.element,{attributes:true,attributeOldValue:true});this.refresh()}}stop(){if(this.started){this.mutationObserver.takeRecords();this.mutationObserver.disconnect();this.started=false}}refresh(){if(this.started)for(const e of this.knownAttributeNames)this.refreshAttribute(e,null)}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){const t=e.attributeName;t&&this.refreshAttribute(t,e.oldValue)}refreshAttribute(e,t){const r=this.delegate.getStringMapKeyForAttribute(e);if(null!=r){this.stringMap.has(e)||this.stringMapKeyAdded(r,e);const s=this.element.getAttribute(e);this.stringMap.get(e)!=s&&this.stringMapValueChanged(s,r,t);if(null==s){const t=this.stringMap.get(e);this.stringMap.delete(e);t&&this.stringMapKeyRemoved(r,e,t)}else this.stringMap.set(e,s)}}stringMapKeyAdded(e,t){this.delegate.stringMapKeyAdded&&this.delegate.stringMapKeyAdded(e,t)}stringMapValueChanged(e,t,r){this.delegate.stringMapValueChanged&&this.delegate.stringMapValueChanged(e,t,r)}stringMapKeyRemoved(e,t,r){this.delegate.stringMapKeyRemoved&&this.delegate.stringMapKeyRemoved(e,t,r)}get knownAttributeNames(){return Array.from(new Set(this.currentAttributeNames.concat(this.recordedAttributeNames)))}get currentAttributeNames(){return Array.from(this.element.attributes).map((e=>e.name))}get recordedAttributeNames(){return Array.from(this.stringMap.keys())}}class TokenListObserver{constructor(e,t,r){this.attributeObserver=new AttributeObserver(e,t,this);this.delegate=r;this.tokensByElement=new Multimap}get started(){return this.attributeObserver.started}start(){this.attributeObserver.start()}pause(e){this.attributeObserver.pause(e)}stop(){this.attributeObserver.stop()}refresh(){this.attributeObserver.refresh()}get element(){return this.attributeObserver.element}get attributeName(){return this.attributeObserver.attributeName}elementMatchedAttribute(e){this.tokensMatched(this.readTokensForElement(e))}elementAttributeValueChanged(e){const[t,r]=this.refreshTokensForElement(e);this.tokensUnmatched(t);this.tokensMatched(r)}elementUnmatchedAttribute(e){this.tokensUnmatched(this.tokensByElement.getValuesForKey(e))}tokensMatched(e){e.forEach((e=>this.tokenMatched(e)))}tokensUnmatched(e){e.forEach((e=>this.tokenUnmatched(e)))}tokenMatched(e){this.delegate.tokenMatched(e);this.tokensByElement.add(e.element,e)}tokenUnmatched(e){this.delegate.tokenUnmatched(e);this.tokensByElement.delete(e.element,e)}refreshTokensForElement(e){const t=this.tokensByElement.getValuesForKey(e);const r=this.readTokensForElement(e);const s=zip(t,r).findIndex((([e,t])=>!tokensAreEqual(e,t)));return-1==s?[[],[]]:[t.slice(s),r.slice(s)]}readTokensForElement(e){const t=this.attributeName;const r=e.getAttribute(t)||"";return parseTokenString(r,e,t)}}function parseTokenString(e,t,r){return e.trim().split(/\s+/).filter((e=>e.length)).map(((e,s)=>({element:t,attributeName:r,content:e,index:s})))}function zip(e,t){const r=Math.max(e.length,t.length);return Array.from({length:r},((r,s)=>[e[s],t[s]]))}function tokensAreEqual(e,t){return e&&t&&e.index==t.index&&e.content==t.content}class ValueListObserver{constructor(e,t,r){this.tokenListObserver=new TokenListObserver(e,t,this);this.delegate=r;this.parseResultsByToken=new WeakMap;this.valuesByTokenByElement=new WeakMap}get started(){return this.tokenListObserver.started}start(){this.tokenListObserver.start()}stop(){this.tokenListObserver.stop()}refresh(){this.tokenListObserver.refresh()}get element(){return this.tokenListObserver.element}get attributeName(){return this.tokenListObserver.attributeName}tokenMatched(e){const{element:t}=e;const{value:r}=this.fetchParseResultForToken(e);if(r){this.fetchValuesByTokenForElement(t).set(e,r);this.delegate.elementMatchedValue(t,r)}}tokenUnmatched(e){const{element:t}=e;const{value:r}=this.fetchParseResultForToken(e);if(r){this.fetchValuesByTokenForElement(t).delete(e);this.delegate.elementUnmatchedValue(t,r)}}fetchParseResultForToken(e){let t=this.parseResultsByToken.get(e);if(!t){t=this.parseToken(e);this.parseResultsByToken.set(e,t)}return t}fetchValuesByTokenForElement(e){let t=this.valuesByTokenByElement.get(e);if(!t){t=new Map;this.valuesByTokenByElement.set(e,t)}return t}parseToken(e){try{const t=this.delegate.parseValueForToken(e);return{value:t}}catch(e){return{error:e}}}}class BindingObserver{constructor(e,t){this.context=e;this.delegate=t;this.bindingsByAction=new Map}start(){if(!this.valueListObserver){this.valueListObserver=new ValueListObserver(this.element,this.actionAttribute,this);this.valueListObserver.start()}}stop(){if(this.valueListObserver){this.valueListObserver.stop();delete this.valueListObserver;this.disconnectAllActions()}}get element(){return this.context.element}get identifier(){return this.context.identifier}get actionAttribute(){return this.schema.actionAttribute}get schema(){return this.context.schema}get bindings(){return Array.from(this.bindingsByAction.values())}connectAction(e){const t=new Binding(this.context,e);this.bindingsByAction.set(e,t);this.delegate.bindingConnected(t)}disconnectAction(e){const t=this.bindingsByAction.get(e);if(t){this.bindingsByAction.delete(e);this.delegate.bindingDisconnected(t)}}disconnectAllActions(){this.bindings.forEach((e=>this.delegate.bindingDisconnected(e,true)));this.bindingsByAction.clear()}parseValueForToken(e){const t=Action.forToken(e,this.schema);if(t.identifier==this.identifier)return t}elementMatchedValue(e,t){this.connectAction(t)}elementUnmatchedValue(e,t){this.disconnectAction(t)}}class ValueObserver{constructor(e,t){this.context=e;this.receiver=t;this.stringMapObserver=new StringMapObserver(this.element,this);this.valueDescriptorMap=this.controller.valueDescriptorMap}start(){this.stringMapObserver.start();this.invokeChangedCallbacksForDefaultValues()}stop(){this.stringMapObserver.stop()}get element(){return this.context.element}get controller(){return this.context.controller}getStringMapKeyForAttribute(e){if(e in this.valueDescriptorMap)return this.valueDescriptorMap[e].name}stringMapKeyAdded(e,t){const r=this.valueDescriptorMap[t];this.hasValue(e)||this.invokeChangedCallback(e,r.writer(this.receiver[e]),r.writer(r.defaultValue))}stringMapValueChanged(e,t,r){const s=this.valueDescriptorNameMap[t];if(null!==e){null===r&&(r=s.writer(s.defaultValue));this.invokeChangedCallback(t,e,r)}}stringMapKeyRemoved(e,t,r){const s=this.valueDescriptorNameMap[e];this.hasValue(e)?this.invokeChangedCallback(e,s.writer(this.receiver[e]),r):this.invokeChangedCallback(e,s.writer(s.defaultValue),r)}invokeChangedCallbacksForDefaultValues(){for(const{key:e,name:t,defaultValue:r,writer:s}of this.valueDescriptors)void 0==r||this.controller.data.has(e)||this.invokeChangedCallback(t,s(r),void 0)}invokeChangedCallback(e,t,r){const s=`${e}Changed`;const n=this.receiver[s];if("function"==typeof n){const s=this.valueDescriptorNameMap[e];try{const e=s.reader(t);let i=r;r&&(i=s.reader(r));n.call(this.receiver,e,i)}catch(e){e instanceof TypeError&&(e.message=`Stimulus Value "${this.context.identifier}.${s.name}" - ${e.message}`);throw e}}}get valueDescriptors(){const{valueDescriptorMap:e}=this;return Object.keys(e).map((t=>e[t]))}get valueDescriptorNameMap(){const e={};Object.keys(this.valueDescriptorMap).forEach((t=>{const r=this.valueDescriptorMap[t];e[r.name]=r}));return e}hasValue(e){const t=this.valueDescriptorNameMap[e];const r=`has${capitalize(t.name)}`;return this.receiver[r]}}class TargetObserver{constructor(e,t){this.context=e;this.delegate=t;this.targetsByName=new Multimap}start(){if(!this.tokenListObserver){this.tokenListObserver=new TokenListObserver(this.element,this.attributeName,this);this.tokenListObserver.start()}}stop(){if(this.tokenListObserver){this.disconnectAllTargets();this.tokenListObserver.stop();delete this.tokenListObserver}}tokenMatched({element:e,content:t}){this.scope.containsElement(e)&&this.connectTarget(e,t)}tokenUnmatched({element:e,content:t}){this.disconnectTarget(e,t)}connectTarget(e,t){var r;if(!this.targetsByName.has(t,e)){this.targetsByName.add(t,e);null===(r=this.tokenListObserver)||void 0===r?void 0:r.pause((()=>this.delegate.targetConnected(e,t)))}}disconnectTarget(e,t){var r;if(this.targetsByName.has(t,e)){this.targetsByName.delete(t,e);null===(r=this.tokenListObserver)||void 0===r?void 0:r.pause((()=>this.delegate.targetDisconnected(e,t)))}}disconnectAllTargets(){for(const e of this.targetsByName.keys)for(const t of this.targetsByName.getValuesForKey(e))this.disconnectTarget(t,e)}get attributeName(){return`data-${this.context.identifier}-target`}get element(){return this.context.element}get scope(){return this.context.scope}}function readInheritableStaticArrayValues(e,t){const r=getAncestorsForConstructor(e);return Array.from(r.reduce(((e,r)=>{getOwnStaticArrayValues(r,t).forEach((t=>e.add(t)));return e}),new Set))}function readInheritableStaticObjectPairs(e,t){const r=getAncestorsForConstructor(e);return r.reduce(((e,r)=>{e.push(...getOwnStaticObjectPairs(r,t));return e}),[])}function getAncestorsForConstructor(e){const t=[];while(e){t.push(e);e=Object.getPrototypeOf(e)}return t.reverse()}function getOwnStaticArrayValues(e,t){const r=e[t];return Array.isArray(r)?r:[]}function getOwnStaticObjectPairs(e,t){const r=e[t];return r?Object.keys(r).map((e=>[e,r[e]])):[]}class OutletObserver{constructor(e,t){this.started=false;this.context=e;this.delegate=t;this.outletsByName=new Multimap;this.outletElementsByName=new Multimap;this.selectorObserverMap=new Map;this.attributeObserverMap=new Map}start(){if(!this.started){this.outletDefinitions.forEach((e=>{this.setupSelectorObserverForOutlet(e);this.setupAttributeObserverForOutlet(e)}));this.started=true;this.dependentContexts.forEach((e=>e.refresh()))}}refresh(){this.selectorObserverMap.forEach((e=>e.refresh()));this.attributeObserverMap.forEach((e=>e.refresh()))}stop(){if(this.started){this.started=false;this.disconnectAllOutlets();this.stopSelectorObservers();this.stopAttributeObservers()}}stopSelectorObservers(){if(this.selectorObserverMap.size>0){this.selectorObserverMap.forEach((e=>e.stop()));this.selectorObserverMap.clear()}}stopAttributeObservers(){if(this.attributeObserverMap.size>0){this.attributeObserverMap.forEach((e=>e.stop()));this.attributeObserverMap.clear()}}selectorMatched(e,t,{outletName:r}){const s=this.getOutlet(e,r);s&&this.connectOutlet(s,e,r)}selectorUnmatched(e,t,{outletName:r}){const s=this.getOutletFromMap(e,r);s&&this.disconnectOutlet(s,e,r)}selectorMatchElement(e,{outletName:t}){const r=this.selector(t);const s=this.hasOutlet(e,t);const n=e.matches(`[${this.schema.controllerAttribute}~=${t}]`);return!!r&&(s&&n&&e.matches(r))}elementMatchedAttribute(e,t){const r=this.getOutletNameFromOutletAttributeName(t);r&&this.updateSelectorObserverForOutlet(r)}elementAttributeValueChanged(e,t){const r=this.getOutletNameFromOutletAttributeName(t);r&&this.updateSelectorObserverForOutlet(r)}elementUnmatchedAttribute(e,t){const r=this.getOutletNameFromOutletAttributeName(t);r&&this.updateSelectorObserverForOutlet(r)}connectOutlet(e,t,r){var s;if(!this.outletElementsByName.has(r,t)){this.outletsByName.add(r,e);this.outletElementsByName.add(r,t);null===(s=this.selectorObserverMap.get(r))||void 0===s?void 0:s.pause((()=>this.delegate.outletConnected(e,t,r)))}}disconnectOutlet(e,t,r){var s;if(this.outletElementsByName.has(r,t)){this.outletsByName.delete(r,e);this.outletElementsByName.delete(r,t);null===(s=this.selectorObserverMap.get(r))||void 0===s?void 0:s.pause((()=>this.delegate.outletDisconnected(e,t,r)))}}disconnectAllOutlets(){for(const e of this.outletElementsByName.keys)for(const t of this.outletElementsByName.getValuesForKey(e))for(const r of this.outletsByName.getValuesForKey(e))this.disconnectOutlet(r,t,e)}updateSelectorObserverForOutlet(e){const t=this.selectorObserverMap.get(e);t&&(t.selector=this.selector(e))}setupSelectorObserverForOutlet(e){const t=this.selector(e);const r=new SelectorObserver(document.body,t,this,{outletName:e});this.selectorObserverMap.set(e,r);r.start()}setupAttributeObserverForOutlet(e){const t=this.attributeNameForOutletName(e);const r=new AttributeObserver(this.scope.element,t,this);this.attributeObserverMap.set(e,r);r.start()}selector(e){return this.scope.outlets.getSelectorForOutletName(e)}attributeNameForOutletName(e){return this.scope.schema.outletAttributeForScope(this.identifier,e)}getOutletNameFromOutletAttributeName(e){return this.outletDefinitions.find((t=>this.attributeNameForOutletName(t)===e))}get outletDependencies(){const e=new Multimap;this.router.modules.forEach((t=>{const r=t.definition.controllerConstructor;const s=readInheritableStaticArrayValues(r,"outlets");s.forEach((r=>e.add(r,t.identifier)))}));return e}get outletDefinitions(){return this.outletDependencies.getKeysForValue(this.identifier)}get dependentControllerIdentifiers(){return this.outletDependencies.getValuesForKey(this.identifier)}get dependentContexts(){const e=this.dependentControllerIdentifiers;return this.router.contexts.filter((t=>e.includes(t.identifier)))}hasOutlet(e,t){return!!this.getOutlet(e,t)||!!this.getOutletFromMap(e,t)}getOutlet(e,t){return this.application.getControllerForElementAndIdentifier(e,t)}getOutletFromMap(e,t){return this.outletsByName.getValuesForKey(t).find((t=>t.element===e))}get scope(){return this.context.scope}get schema(){return this.context.schema}get identifier(){return this.context.identifier}get application(){return this.context.application}get router(){return this.application.router}}class Context{constructor(e,t){this.logDebugActivity=(e,t={})=>{const{identifier:r,controller:s,element:n}=this;t=Object.assign({identifier:r,controller:s,element:n},t);this.application.logDebugActivity(this.identifier,e,t)};this.module=e;this.scope=t;this.controller=new e.controllerConstructor(this);this.bindingObserver=new BindingObserver(this,this.dispatcher);this.valueObserver=new ValueObserver(this,this.controller);this.targetObserver=new TargetObserver(this,this);this.outletObserver=new OutletObserver(this,this);try{this.controller.initialize();this.logDebugActivity("initialize")}catch(e){this.handleError(e,"initializing controller")}}connect(){this.bindingObserver.start();this.valueObserver.start();this.targetObserver.start();this.outletObserver.start();try{this.controller.connect();this.logDebugActivity("connect")}catch(e){this.handleError(e,"connecting controller")}}refresh(){this.outletObserver.refresh()}disconnect(){try{this.controller.disconnect();this.logDebugActivity("disconnect")}catch(e){this.handleError(e,"disconnecting controller")}this.outletObserver.stop();this.targetObserver.stop();this.valueObserver.stop();this.bindingObserver.stop()}get application(){return this.module.application}get identifier(){return this.module.identifier}get schema(){return this.application.schema}get dispatcher(){return this.application.dispatcher}get element(){return this.scope.element}get parentElement(){return this.element.parentElement}handleError(e,t,r={}){const{identifier:s,controller:n,element:i}=this;r=Object.assign({identifier:s,controller:n,element:i},r);this.application.handleError(e,`Error ${t}`,r)}targetConnected(e,t){this.invokeControllerMethod(`${t}TargetConnected`,e)}targetDisconnected(e,t){this.invokeControllerMethod(`${t}TargetDisconnected`,e)}outletConnected(e,t,r){this.invokeControllerMethod(`${namespaceCamelize(r)}OutletConnected`,e,t)}outletDisconnected(e,t,r){this.invokeControllerMethod(`${namespaceCamelize(r)}OutletDisconnected`,e,t)}invokeControllerMethod(e,...t){const r=this.controller;"function"==typeof r[e]&&r[e](...t)}}function bless(e){return shadow(e,getBlessedProperties(e))}function shadow(e,t){const r=i(e);const s=getShadowProperties(e.prototype,t);Object.defineProperties(r.prototype,s);return r}function getBlessedProperties(e){const t=readInheritableStaticArrayValues(e,"blessings");return t.reduce(((t,r)=>{const s=r(e);for(const e in s){const r=t[e]||{};t[e]=Object.assign(r,s[e])}return t}),{})}function getShadowProperties(e,t){return n(t).reduce(((r,s)=>{const n=getShadowedDescriptor(e,t,s);n&&Object.assign(r,{[s]:n});return r}),{})}function getShadowedDescriptor(e,t,r){const s=Object.getOwnPropertyDescriptor(e,r);const n=s&&"value"in s;if(!n){const e=Object.getOwnPropertyDescriptor(t,r).value;if(s){e.get=s.get||e.get;e.set=s.set||e.set}return e}}const n=(()=>"function"==typeof Object.getOwnPropertySymbols?e=>[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)]:Object.getOwnPropertyNames)();const i=(()=>{function extendWithReflect(e){function extended(){return Reflect.construct(e,arguments,new.target)}extended.prototype=Object.create(e.prototype,{constructor:{value:extended}});Reflect.setPrototypeOf(extended,e);return extended}function testReflectExtension(){const a=function(){this.a.call(this)};const e=extendWithReflect(a);e.prototype.a=function(){};return new e}try{testReflectExtension();return extendWithReflect}catch(e){return e=>class extended extends e{}}})();function blessDefinition(e){return{identifier:e.identifier,controllerConstructor:bless(e.controllerConstructor)}}class Module{constructor(e,t){this.application=e;this.definition=blessDefinition(t);this.contextsByScope=new WeakMap;this.connectedContexts=new Set}get identifier(){return this.definition.identifier}get controllerConstructor(){return this.definition.controllerConstructor}get contexts(){return Array.from(this.connectedContexts)}connectContextForScope(e){const t=this.fetchContextForScope(e);this.connectedContexts.add(t);t.connect()}disconnectContextForScope(e){const t=this.contextsByScope.get(e);if(t){this.connectedContexts.delete(t);t.disconnect()}}fetchContextForScope(e){let t=this.contextsByScope.get(e);if(!t){t=new Context(this,e);this.contextsByScope.set(e,t)}return t}}class ClassMap{constructor(e){this.scope=e}has(e){return this.data.has(this.getDataKey(e))}get(e){return this.getAll(e)[0]}getAll(e){const t=this.data.get(this.getDataKey(e))||"";return tokenize(t)}getAttributeName(e){return this.data.getAttributeNameForKey(this.getDataKey(e))}getDataKey(e){return`${e}-class`}get data(){return this.scope.data}}class DataMap{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get(e){const t=this.getAttributeNameForKey(e);return this.element.getAttribute(t)}set(e,t){const r=this.getAttributeNameForKey(e);this.element.setAttribute(r,t);return this.get(e)}has(e){const t=this.getAttributeNameForKey(e);return this.element.hasAttribute(t)}delete(e){if(this.has(e)){const t=this.getAttributeNameForKey(e);this.element.removeAttribute(t);return true}return false}getAttributeNameForKey(e){return`data-${this.identifier}-${dasherize(e)}`}}class Guide{constructor(e){this.warnedKeysByObject=new WeakMap;this.logger=e}warn(e,t,r){let s=this.warnedKeysByObject.get(e);if(!s){s=new Set;this.warnedKeysByObject.set(e,s)}if(!s.has(t)){s.add(t);this.logger.warn(r,e)}}}function attributeValueContainsToken(e,t){return`[${e}~="${t}"]`}class TargetSet{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce(((e,t)=>e||this.findTarget(t)||this.findLegacyTarget(t)),void 0)}findAll(...e){return e.reduce(((e,t)=>[...e,...this.findAllTargets(t),...this.findAllLegacyTargets(t)]),[])}findTarget(e){const t=this.getSelectorForTargetName(e);return this.scope.findElement(t)}findAllTargets(e){const t=this.getSelectorForTargetName(e);return this.scope.findAllElements(t)}getSelectorForTargetName(e){const t=this.schema.targetAttributeForScope(this.identifier);return attributeValueContainsToken(t,e)}findLegacyTarget(e){const t=this.getLegacySelectorForTargetName(e);return this.deprecate(this.scope.findElement(t),e)}findAllLegacyTargets(e){const t=this.getLegacySelectorForTargetName(e);return this.scope.findAllElements(t).map((t=>this.deprecate(t,e)))}getLegacySelectorForTargetName(e){const t=`${this.identifier}.${e}`;return attributeValueContainsToken(this.schema.targetAttribute,t)}deprecate(e,t){if(e){const{identifier:r}=this;const s=this.schema.targetAttribute;const n=this.schema.targetAttributeForScope(r);this.guide.warn(e,`target:${t}`,`Please replace ${s}="${r}.${t}" with ${n}="${t}". The ${s} attribute is deprecated and will be removed in a future version of Stimulus.`)}return e}get guide(){return this.scope.guide}}class OutletSet{constructor(e,t){this.scope=e;this.controllerElement=t}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce(((e,t)=>e||this.findOutlet(t)),void 0)}findAll(...e){return e.reduce(((e,t)=>[...e,...this.findAllOutlets(t)]),[])}getSelectorForOutletName(e){const t=this.schema.outletAttributeForScope(this.identifier,e);return this.controllerElement.getAttribute(t)}findOutlet(e){const t=this.getSelectorForOutletName(e);if(t)return this.findElement(t,e)}findAllOutlets(e){const t=this.getSelectorForOutletName(e);return t?this.findAllElements(t,e):[]}findElement(e,t){const r=this.scope.queryElements(e);return r.filter((r=>this.matchesElement(r,e,t)))[0]}findAllElements(e,t){const r=this.scope.queryElements(e);return r.filter((r=>this.matchesElement(r,e,t)))}matchesElement(e,t,r){const s=e.getAttribute(this.scope.schema.controllerAttribute)||"";return e.matches(t)&&s.split(" ").includes(r)}}class Scope{constructor(e,t,r,s){this.targets=new TargetSet(this);this.classes=new ClassMap(this);this.data=new DataMap(this);this.containsElement=e=>e.closest(this.controllerSelector)===this.element;this.schema=e;this.element=t;this.identifier=r;this.guide=new Guide(s);this.outlets=new OutletSet(this.documentScope,t)}findElement(e){return this.element.matches(e)?this.element:this.queryElements(e).find(this.containsElement)}findAllElements(e){return[...this.element.matches(e)?[this.element]:[],...this.queryElements(e).filter(this.containsElement)]}queryElements(e){return Array.from(this.element.querySelectorAll(e))}get controllerSelector(){return attributeValueContainsToken(this.schema.controllerAttribute,this.identifier)}get isDocumentScope(){return this.element===document.documentElement}get documentScope(){return this.isDocumentScope?this:new Scope(this.schema,document.documentElement,this.identifier,this.guide.logger)}}class ScopeObserver{constructor(e,t,r){this.element=e;this.schema=t;this.delegate=r;this.valueListObserver=new ValueListObserver(this.element,this.controllerAttribute,this);this.scopesByIdentifierByElement=new WeakMap;this.scopeReferenceCounts=new WeakMap}start(){this.valueListObserver.start()}stop(){this.valueListObserver.stop()}get controllerAttribute(){return this.schema.controllerAttribute}parseValueForToken(e){const{element:t,content:r}=e;return this.parseValueForElementAndIdentifier(t,r)}parseValueForElementAndIdentifier(e,t){const r=this.fetchScopesByIdentifierForElement(e);let s=r.get(t);if(!s){s=this.delegate.createScopeForElementAndIdentifier(e,t);r.set(t,s)}return s}elementMatchedValue(e,t){const r=(this.scopeReferenceCounts.get(t)||0)+1;this.scopeReferenceCounts.set(t,r);1==r&&this.delegate.scopeConnected(t)}elementUnmatchedValue(e,t){const r=this.scopeReferenceCounts.get(t);if(r){this.scopeReferenceCounts.set(t,r-1);1==r&&this.delegate.scopeDisconnected(t)}}fetchScopesByIdentifierForElement(e){let t=this.scopesByIdentifierByElement.get(e);if(!t){t=new Map;this.scopesByIdentifierByElement.set(e,t)}return t}}class Router{constructor(e){this.application=e;this.scopeObserver=new ScopeObserver(this.element,this.schema,this);this.scopesByIdentifier=new Multimap;this.modulesByIdentifier=new Map}get element(){return this.application.element}get schema(){return this.application.schema}get logger(){return this.application.logger}get controllerAttribute(){return this.schema.controllerAttribute}get modules(){return Array.from(this.modulesByIdentifier.values())}get contexts(){return this.modules.reduce(((e,t)=>e.concat(t.contexts)),[])}start(){this.scopeObserver.start()}stop(){this.scopeObserver.stop()}loadDefinition(e){this.unloadIdentifier(e.identifier);const t=new Module(this.application,e);this.connectModule(t);const r=e.controllerConstructor.afterLoad;r&&r.call(e.controllerConstructor,e.identifier,this.application)}unloadIdentifier(e){const t=this.modulesByIdentifier.get(e);t&&this.disconnectModule(t)}getContextForElementAndIdentifier(e,t){const r=this.modulesByIdentifier.get(t);if(r)return r.contexts.find((t=>t.element==e))}proposeToConnectScopeForElementAndIdentifier(e,t){const r=this.scopeObserver.parseValueForElementAndIdentifier(e,t);r?this.scopeObserver.elementMatchedValue(r.element,r):console.error(`Couldn't find or create scope for identifier: "${t}" and element:`,e)}handleError(e,t,r){this.application.handleError(e,t,r)}createScopeForElementAndIdentifier(e,t){return new Scope(this.schema,e,t,this.logger)}scopeConnected(e){this.scopesByIdentifier.add(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.connectContextForScope(e)}scopeDisconnected(e){this.scopesByIdentifier.delete(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.disconnectContextForScope(e)}connectModule(e){this.modulesByIdentifier.set(e.identifier,e);const t=this.scopesByIdentifier.getValuesForKey(e.identifier);t.forEach((t=>e.connectContextForScope(t)))}disconnectModule(e){this.modulesByIdentifier.delete(e.identifier);const t=this.scopesByIdentifier.getValuesForKey(e.identifier);t.forEach((t=>e.disconnectContextForScope(t)))}}const o={controllerAttribute:"data-controller",actionAttribute:"data-action",targetAttribute:"data-target",targetAttributeForScope:e=>`data-${e}-target`,outletAttributeForScope:(e,t)=>`data-${e}-${t}-outlet`,keyMappings:Object.assign(Object.assign({enter:"Enter",tab:"Tab",esc:"Escape",space:" ",up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",page_up:"PageUp",page_down:"PageDown"},objectFromEntries("abcdefghijklmnopqrstuvwxyz".split("").map((e=>[e,e])))),objectFromEntries("0123456789".split("").map((e=>[e,e]))))};function objectFromEntries(e){return e.reduce(((e,[t,r])=>Object.assign(Object.assign({},e),{[t]:r})),{})}class Application{constructor(t=document.documentElement,r=o){this.logger=console;this.debug=false;this.logDebugActivity=(e,t,r={})=>{this.debug&&this.logFormattedMessage(e,t,r)};this.element=t;this.schema=r;this.dispatcher=new Dispatcher(this);this.router=new Router(this);this.actionDescriptorFilters=Object.assign({},e)}static start(e,t){const r=new this(e,t);r.start();return r}async start(){await domReady();this.logDebugActivity("application","starting");this.dispatcher.start();this.router.start();this.logDebugActivity("application","start")}stop(){this.logDebugActivity("application","stopping");this.dispatcher.stop();this.router.stop();this.logDebugActivity("application","stop")}register(e,t){this.load({identifier:e,controllerConstructor:t})}registerActionOption(e,t){this.actionDescriptorFilters[e]=t}load(e,...t){const r=Array.isArray(e)?e:[e,...t];r.forEach((e=>{e.controllerConstructor.shouldLoad&&this.router.loadDefinition(e)}))}unload(e,...t){const r=Array.isArray(e)?e:[e,...t];r.forEach((e=>this.router.unloadIdentifier(e)))}get controllers(){return this.router.contexts.map((e=>e.controller))}getControllerForElementAndIdentifier(e,t){const r=this.router.getContextForElementAndIdentifier(e,t);return r?r.controller:null}handleError(e,t,r){var s;this.logger.error("%s\n\n%o\n\n%o",t,e,r);null===(s=window.onerror)||void 0===s?void 0:s.call(window,t,"",0,0,e)}logFormattedMessage(e,t,r={}){r=Object.assign({application:this},r);this.logger.groupCollapsed(`${e} #${t}`);this.logger.log("details:",Object.assign({},r));this.logger.groupEnd()}}function domReady(){return new Promise((e=>{"loading"==document.readyState?document.addEventListener("DOMContentLoaded",(()=>e())):e()}))}function ClassPropertiesBlessing(e){const t=readInheritableStaticArrayValues(e,"classes");return t.reduce(((e,t)=>Object.assign(e,propertiesForClassDefinition(t))),{})}function propertiesForClassDefinition(e){return{[`${e}Class`]:{get(){const{classes:t}=this;if(t.has(e))return t.get(e);{const r=t.getAttributeName(e);throw new Error(`Missing attribute "${r}"`)}}},[`${e}Classes`]:{get(){return this.classes.getAll(e)}},[`has${capitalize(e)}Class`]:{get(){return this.classes.has(e)}}}}function OutletPropertiesBlessing(e){const t=readInheritableStaticArrayValues(e,"outlets");return t.reduce(((e,t)=>Object.assign(e,propertiesForOutletDefinition(t))),{})}function getOutletController(e,t,r){return e.application.getControllerForElementAndIdentifier(t,r)}function getControllerAndEnsureConnectedScope(e,t,r){let s=getOutletController(e,t,r);if(s)return s;e.application.router.proposeToConnectScopeForElementAndIdentifier(t,r);s=getOutletController(e,t,r);return s||void 0}function propertiesForOutletDefinition(e){const t=namespaceCamelize(e);return{[`${t}Outlet`]:{get(){const t=this.outlets.find(e);const r=this.outlets.getSelectorForOutletName(e);if(t){const r=getControllerAndEnsureConnectedScope(this,t,e);if(r)return r;throw new Error(`The provided outlet element is missing an outlet controller "${e}" instance for host controller "${this.identifier}"`)}throw new Error(`Missing outlet element "${e}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${r}".`)}},[`${t}Outlets`]:{get(){const t=this.outlets.findAll(e);return t.length>0?t.map((t=>{const r=getControllerAndEnsureConnectedScope(this,t,e);if(r)return r;console.warn(`The provided outlet element is missing an outlet controller "${e}" instance for host controller "${this.identifier}"`,t)})).filter((e=>e)):[]}},[`${t}OutletElement`]:{get(){const t=this.outlets.find(e);const r=this.outlets.getSelectorForOutletName(e);if(t)return t;throw new Error(`Missing outlet element "${e}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${r}".`)}},[`${t}OutletElements`]:{get(){return this.outlets.findAll(e)}},[`has${capitalize(t)}Outlet`]:{get(){return this.outlets.has(e)}}}}function TargetPropertiesBlessing(e){const t=readInheritableStaticArrayValues(e,"targets");return t.reduce(((e,t)=>Object.assign(e,propertiesForTargetDefinition(t))),{})}function propertiesForTargetDefinition(e){return{[`${e}Target`]:{get(){const t=this.targets.find(e);if(t)return t;throw new Error(`Missing target element "${e}" for "${this.identifier}" controller`)}},[`${e}Targets`]:{get(){return this.targets.findAll(e)}},[`has${capitalize(e)}Target`]:{get(){return this.targets.has(e)}}}}function ValuePropertiesBlessing(e){const t=readInheritableStaticObjectPairs(e,"values");const r={valueDescriptorMap:{get(){return t.reduce(((e,t)=>{const r=parseValueDefinitionPair(t,this.identifier);const s=this.data.getAttributeNameForKey(r.key);return Object.assign(e,{[s]:r})}),{})}}};return t.reduce(((e,t)=>Object.assign(e,propertiesForValueDefinitionPair(t))),r)}function propertiesForValueDefinitionPair(e,t){const r=parseValueDefinitionPair(e,t);const{key:s,name:n,reader:i,writer:o}=r;return{[n]:{get(){const e=this.data.get(s);return null!==e?i(e):r.defaultValue},set(e){void 0===e?this.data.delete(s):this.data.set(s,o(e))}},[`has${capitalize(n)}`]:{get(){return this.data.has(s)||r.hasCustomDefaultValue}}}}function parseValueDefinitionPair([e,t],r){return valueDescriptorForTokenAndTypeDefinition({controller:r,token:e,typeDefinition:t})}function parseValueTypeConstant(e){switch(e){case Array:return"array";case Boolean:return"boolean";case Number:return"number";case Object:return"object";case String:return"string"}}function parseValueTypeDefault(e){switch(typeof e){case"boolean":return"boolean";case"number":return"number";case"string":return"string"}return Array.isArray(e)?"array":"[object Object]"===Object.prototype.toString.call(e)?"object":void 0}function parseValueTypeObject(e){const{controller:t,token:r,typeObject:s}=e;const n=isSomething(s.type);const i=isSomething(s.default);const o=n&&i;const c=n&&!i;const l=!n&&i;const h=parseValueTypeConstant(s.type);const u=parseValueTypeDefault(e.typeObject.default);if(c)return h;if(l)return u;if(h!==u){const e=t?`${t}.${r}`:r;throw new Error(`The specified default value for the Stimulus Value "${e}" must match the defined type "${h}". The provided default value of "${s.default}" is of type "${u}".`)}return o?h:void 0}function parseValueTypeDefinition(e){const{controller:t,token:r,typeDefinition:s}=e;const n={controller:t,token:r,typeObject:s};const i=parseValueTypeObject(n);const o=parseValueTypeDefault(s);const c=parseValueTypeConstant(s);const l=i||o||c;if(l)return l;const h=t?`${t}.${s}`:r;throw new Error(`Unknown value type "${h}" for "${r}" value`)}function defaultValueForDefinition(e){const t=parseValueTypeConstant(e);if(t)return c[t];const r=hasProperty(e,"default");const s=hasProperty(e,"type");const n=e;if(r)return n.default;if(s){const{type:e}=n;const t=parseValueTypeConstant(e);if(t)return c[t]}return e}function valueDescriptorForTokenAndTypeDefinition(e){const{token:t,typeDefinition:r}=e;const s=`${dasherize(t)}-value`;const n=parseValueTypeDefinition(e);return{type:n,key:s,name:camelize(s),get defaultValue(){return defaultValueForDefinition(r)},get hasCustomDefaultValue(){return void 0!==parseValueTypeDefault(r)},reader:l[n],writer:h[n]||h.default}}const c={get array(){return[]},boolean:false,number:0,get object(){return{}},string:""};const l={array(e){const t=JSON.parse(e);if(!Array.isArray(t))throw new TypeError(`expected value of type "array" but instead got value "${e}" of type "${parseValueTypeDefault(t)}"`);return t},boolean(e){return!("0"==e||"false"==String(e).toLowerCase())},number(e){return Number(e.replace(/_/g,""))},object(e){const t=JSON.parse(e);if(null===t||"object"!=typeof t||Array.isArray(t))throw new TypeError(`expected value of type "object" but instead got value "${e}" of type "${parseValueTypeDefault(t)}"`);return t},string(e){return e}};const h={default:writeString,array:writeJSON,object:writeJSON};function writeJSON(e){return JSON.stringify(e)}function writeString(e){return`${e}`}class Controller{constructor(e){this.context=e}static get shouldLoad(){return true}static afterLoad(e,t){}get application(){return this.context.application}get scope(){return this.context.scope}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get targets(){return this.scope.targets}get outlets(){return this.scope.outlets}get classes(){return this.scope.classes}get data(){return this.scope.data}initialize(){}connect(){}disconnect(){}dispatch(e,{target:t=this.element,detail:r={},prefix:s=this.identifier,bubbles:n=true,cancelable:i=true}={}){const o=s?`${s}:${e}`:e;const c=new CustomEvent(o,{detail:r,bubbles:n,cancelable:i});t.dispatchEvent(c);return c}}Controller.blessings=[ClassPropertiesBlessing,TargetPropertiesBlessing,ValuePropertiesBlessing,OutletPropertiesBlessing];Controller.targets=[];Controller.outlets=[];Controller.values={};export{Application,AttributeObserver,Context,Controller,ElementObserver,IndexedMultimap,Multimap,SelectorObserver,StringMapObserver,TokenListObserver,ValueListObserver,add,o as defaultSchema,del,fetch,prune}; + diff --git a/views/bridge-form.ejs b/views/bridge-form.ejs deleted file mode 100644 index 4c6e6e9..0000000 --- a/views/bridge-form.ejs +++ /dev/null @@ -1,36 +0,0 @@ -

Bridge Components

- -

Form example

- -

- This screen contains a form associated with a Bridge form component. It contains a web submit button that submits the form and redirects to a success page after a short delay. -

- -

- Since the Turbo Native demo app supports the form component, the web submit button is hidden and is replaced with a native button in the top-right native app bar. -

- -

- Displaying the submit button in the top-right of the screen is a typical convention in mobile apps, has the benefit of never being hidden underneath the virtual keyboard, and is always visible no matter where you're scrolled on the page. -

- -
- - - - - - - - - -
diff --git a/views/bridge-menu.ejs b/views/bridge-menu.ejs deleted file mode 100644 index 8ca0d12..0000000 --- a/views/bridge-menu.ejs +++ /dev/null @@ -1,74 +0,0 @@ -

Bridge Components

- -

Menu example

- -

- This screen contains a menu associated with a Bridge menu component. The page contains a button below to open a web-based menu that contains several options to choose from. -

- -

- Since the Turbo Native demo app supports the menu component, the web menu is hidden and replaced with a natively displayed menu. Tapping on a native menu option replies back to the web menu component with the selectedIndex of the selected option. -

- -

- Displaying the menu in a native bottom sheet is a typical convention in mobile apps. It blocks touch input for all controls on the screen and allows platform gestures to dismiss the sheet. -

- -
- - - -
- -
- - × -

Select an option

- - - - - - - - - -
- -
- -

- -

- -
diff --git a/views/bridge-overflow.ejs b/views/bridge-overflow.ejs deleted file mode 100644 index 1eabb82..0000000 --- a/views/bridge-overflow.ejs +++ /dev/null @@ -1,76 +0,0 @@ -

Bridge Components

- -

Overflow + Menu example

- -

- This screen contains a button associated with a Bridge overflow-menu component. The page also contains a web-based menu associated with a Bridge menu component that contains several options to choose from. -

- -

- Since the Turbo Native demo app supports the overflow-menu component and the menu component, the web button to open the menu is hidden and replaced with a native 3-dot menu in the top-right native app bar. Tapping on that 3-dot menu will display a native menu driven by the menu component. -

- -

- Hiding the menu options behind the 3-dot overflow-menu is a typical convention in mobile apps. It provides a common place for infrequently used actions and makes more room on the screen for more important actions. -

- -
- - - -
- -
- - × -

Select an option

- - - - - - - - - -
- -
- -

- -

- -
diff --git a/views/files.ejs b/views/files.ejs deleted file mode 100644 index ca1e8c8..0000000 --- a/views/files.ejs +++ /dev/null @@ -1,15 +0,0 @@ -

Handling Files

- -

- Here's a link to an image. Turbo doesn't handle non-html links, so it will ignore this link. By default, it will open in the browser. We've added special handling in the app for files on the same domain to open in-app instead. -

- - - - - -

- - Image credit to Chris Lee via Unsplash - -

diff --git a/views/index.ejs b/views/index.ejs deleted file mode 100644 index 798c7ea..0000000 --- a/views/index.ejs +++ /dev/null @@ -1,91 +0,0 @@ -<% if (locals.authenticated) { %> -
- 👋 Hey <%= locals.authenticated %>! - -
-<% } %> - -

Hotwire Native Demo

-

This demo app will help you get acquainted with the framework.

- - - - - - diff --git a/views/layout.ejs b/views/layout.ejs deleted file mode 100644 index ceb4557..0000000 --- a/views/layout.ejs +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - <%= title %> - - - - - - <% if (locals.native_app) { %> - - - <% } %> - - - - - - "> -
- <%- body %> -
- - diff --git a/views/long.ejs b/views/long.ejs deleted file mode 100644 index cfa76e6..0000000 --- a/views/long.ejs +++ /dev/null @@ -1,53 +0,0 @@ -

A Really Long Page

- -

-Scroll down a bit, then tap a link to navigate forward to another screen. -

- -

Moby Dick

- -

-Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, and nothing particular to interest me on shore, I thought I would sail about a little and see the watery part of the world. It is a way I have of driving off the spleen and regulating the circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing up the rear of every funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires a strong moral principle to prevent me from deliberately stepping into the street, and methodically knocking people’s hats off—then, I account it high time to get to sea as soon as I can. This is my substitute for pistol and ball. With a philosophical flourish Cato throws himself upon his sword; I quietly take to the ship. There is nothing surprising in this. If they but knew it, almost all men in their degree, some time or other, cherish very nearly the same feelings towards the ocean with me. -

- -

Scroll to the bottom of the screen

- -

-There now is your insular city of the Manhattoes, belted round by wharves as Indian isles by coral reefs—commerce surrounds it with her surf. -

- -

Navigate to another screen

- -

-Right and left, the streets take you waterward. Its extreme downtown is the battery, where that noble mole is washed by waves, and cooled by breezes, which a few hours previous were out of sight of land. Look at the crowds of water-gazers there. -

- -

Navigate to another screen

- -

-Circumambulate the city of a dreamy Sabbath afternoon. Go from Corlears Hook to Coenties Slip, and from thence, by Whitehall, northward. What do you see?—Posted like silent sentinels all around the town, stand thousands upon thousands of mortal men fixed in ocean reveries. Some leaning against the spiles; some seated upon the pier-heads; some looking over the bulwarks of ships from China; some high aloft in the rigging, as if striving to get a still better seaward peep. But these are all landsmen; of week days pent up in lath and plaster—tied to counters, nailed to benches, clinched to desks. How then is this? Are the green fields gone? What do they here? -

- -

Navigate to another screen

- -

-But look! here come more crowds, pacing straight for the water, and seemingly bound for a dive. Strange! Nothing will content them but the extremest limit of the land; loitering under the shady lee of yonder warehouses will not suffice. No. They must get just as nigh the water as they possibly can without falling in. And there they stand—miles of them—leagues. Inlanders all, they come from lanes and alleys, streets and avenues—north, east, south, and west. Yet here they all unite. Tell me, does the magnetic virtue of the needles of the compasses of all those ships attract them thither? -

- -

Navigate to another screen

- -

-Once more. Say you are in the country; in some high land of lakes. Take almost any path you please, and ten to one it carries you down in a dale, and leaves you there by a pool in the stream. There is magic in it. Let the most absent-minded of men be plunged in his deepest reveries—stand that man on his legs, set his feet a-going, and he will infallibly lead you to water, if water there be in all that region. Should you ever be athirst in the great American desert, try this experiment, if your caravan happen to be supplied with a metaphysical professor. Yes, as every one knows, meditation and water are wedded for ever. -

- -

Navigate to another screen

- -

-But here is an artist. He desires to paint you the dreamiest, shadiest, quietest, most enchanting bit of romantic landscape in all the valley of the Saco. What is the chief element he employs? There stand his trees, each with a hollow trunk, as if a hermit and a crucifix were within; and here sleeps his meadow, and there sleep his cattle; and up from yonder cottage goes a sleepy smoke. Deep into distant woodlands winds a mazy way, reaching to overlapping spurs of mountains bathed in their hill-side blue. But though the picture lies thus tranced, and though this pine-tree shakes down its sighs like leaves upon this shepherd’s head, yet all were vain, unless the shepherd’s eye were fixed upon the magic stream before him. Go visit the Prairies in June, when for scores on scores of miles you wade knee-deep among Tiger-lilies—what is the one charm wanting?—Water—there is not a drop of water there! Were Niagara but a cataract of sand, would you travel your thousand miles to see it? Why did the poor poet of Tennessee, upon suddenly receiving two handfuls of silver, deliberate whether to buy him a coat, which he sadly needed, or invest his money in a pedestrian trip to Rockaway Beach? Why is almost every robust healthy boy with a robust healthy soul in him, at some time or other crazy to go to sea? Why upon your first voyage as a passenger, did you yourself feel such a mystical vibration, when first told that you and your ship were now out of sight of land? Why did the old Persians hold the sea holy? Why did the Greeks give it a separate deity, and own brother of Jove? Surely all this is not without meaning. And still deeper the meaning of that story of Narcissus, who because he could not grasp the tormenting, mild image he saw in the fountain, plunged into it and was drowned. But that same image, we ourselves see in all rivers and oceans. It is the image of the ungraspable phantom of life; and this is the key to it all. -

- -

Navigate to another screen

- -

-Now, when I say that I am in the habit of going to sea whenever I begin to grow hazy about the eyes, and begin to be over conscious of my lungs, I do not mean to have it inferred that I ever go to sea as a passenger. For to go as a passenger you must needs have a purse, and a purse is but a rag unless you have something in it. Besides, passengers get sea-sick—grow quarrelsome—don’t sleep of nights—do not enjoy themselves much, as a general thing;—no, I never go as a passenger; nor, though I am something of a salt, do I ever go to sea as a Commodore, or a Captain, or a Cook. I abandon the glory and distinction of such offices to those who like them. For my part, I abominate all honorable respectable toils, trials, and tribulations of every kind whatsoever. It is quite as much as I can do to take care of myself, without taking care of ships, barques, brigs, schooners, and what not. And as for going as cook,—though I confess there is considerable glory in that, a cook being a sort of officer on ship-board—yet, somehow, I never fancied broiling fowls;—though once broiled, judiciously buttered, and judgmatically salted and peppered, there is no one who will speak more respectfully, not to say reverentially, of a broiled fowl than I will. It is out of the idolatrous dotings of the old Egyptians upon broiled ibis and roasted river horse, that you see the mummies of those creatures in their huge bake-houses the pyramids. -

diff --git a/views/new.ejs b/views/new.ejs deleted file mode 100644 index 8311b74..0000000 --- a/views/new.ejs +++ /dev/null @@ -1,12 +0,0 @@ -

A Modal Webpage

-

- This screen was presented as a modal based on the path configuration file. Submitting this form will redirect to a success page. The native app will receive that visit proposal and initiate a new visit. -

- -

- Notice that submitting the form will also cause it to dismiss the modal. That's handled by the demo app's router. -

- -
- -
diff --git a/views/numbers.ejs b/views/numbers.ejs deleted file mode 100644 index cc4fe44..0000000 --- a/views/numbers.ejs +++ /dev/null @@ -1,14 +0,0 @@ -

A List of Numbers

- -
    -
  • 1
  • -
  • 2
  • -
  • 3
  • -
  • 4
  • -
  • 5
  • -
  • 6
  • -
  • 7
  • -
  • 8
  • -
  • 9
  • -
  • 10
  • -
\ No newline at end of file diff --git a/views/one.ejs b/views/one.ejs deleted file mode 100644 index 000c793..0000000 --- a/views/one.ejs +++ /dev/null @@ -1,25 +0,0 @@ -

How’d You Get Here?

-

When you tap the link to this page, Turbo proposes a visit to the app to decide how to handle the URL. In this case, it chooses to present a Turbo-based web screen.

-

Go back and try Intercept with a native view to see how to load a native screen instead.

- -

This link will be presented using the same advance visit presentation:

-

Advance to another webpage

- -

This performs a replace visit instead:

-

Replace with another webpage

- -

This performs a replace visit to the current page, refreshing its content with morphing:

-

Refresh

- -

This programatically refreshes the current page, performing a replace visit and refreshing its content with morphing:

-

Refresh programatically

- -
- Rendered <%- new Date().toLocaleString() %> via Turbo 8 -
- - \ No newline at end of file diff --git a/views/protected.ejs b/views/protected.ejs deleted file mode 100644 index c91f6e5..0000000 --- a/views/protected.ejs +++ /dev/null @@ -1,3 +0,0 @@ -

Protected Page

-

This page requires authorization. You can see it because you’re signed in.

-

If you weren’t signed in, the server would have returned a "401 Unauthorized" response.

diff --git a/views/redirected.ejs b/views/redirected.ejs deleted file mode 100644 index 2c83c82..0000000 --- a/views/redirected.ejs +++ /dev/null @@ -1,2 +0,0 @@ -

Redirected Page

-

This page is the result of a redirect. The original destination has been replaced with this page.

diff --git a/views/reference.ejs b/views/reference.ejs deleted file mode 100644 index 08f6c95..0000000 --- a/views/reference.ejs +++ /dev/null @@ -1,25 +0,0 @@ - diff --git a/views/scroll.ejs b/views/scroll.ejs deleted file mode 100644 index e707121..0000000 --- a/views/scroll.ejs +++ /dev/null @@ -1,5 +0,0 @@ -

Restoring Your Scroll

- -

-Now, go back. You'll notice even though we're using a single web view, Turbo automatically restores the scroll position of the previous page and takes a screenshot of the current page to make the transition seamless. -

diff --git a/views/signin.ejs b/views/signin.ejs deleted file mode 100644 index d79d9fa..0000000 --- a/views/signin.ejs +++ /dev/null @@ -1,7 +0,0 @@ -

Sign In

- -
- - - -
diff --git a/views/slow.ejs b/views/slow.ejs deleted file mode 100644 index 83fe8bd..0000000 --- a/views/slow.ejs +++ /dev/null @@ -1,9 +0,0 @@ -

Slow-loading Page

- -

This page is rendered with a delay on the server so you can see the loading indicator and test Turbo's preview cache.

-

To see the preview cache in action, tap the Back button, then return to this page. It will load instantly while the slow network request completes in the background.

-

Tap the Back button and use pull-to-refresh before returning to see the slow, uncached version again.

- -
- Rendered <%- new Date().toLocaleString() %> via Turbo 8 -
diff --git a/views/success.ejs b/views/success.ejs deleted file mode 100644 index 13da1f6..0000000 --- a/views/success.ejs +++ /dev/null @@ -1,5 +0,0 @@ -

It Worked!

- -

- You have successfully submitted a form. What a ride. -

diff --git a/views/turbo-drive.ejs b/views/turbo-drive.ejs deleted file mode 100644 index 02c0053..0000000 --- a/views/turbo-drive.ejs +++ /dev/null @@ -1,32 +0,0 @@ -

- - Turbo Drive: Navigate within a persistent process -

- -

- A key attraction with traditional single-page applications, when compared with the old-school, separate-pages approach, is the speed of navigation. SPAs get a lot of that speed from not constantly tearing down the application process, only to reinitialize it on the very next page. -

- -

- Turbo Drive gives you that same speed by using the same persistent-process model, but without requiring you to craft your entire application around the paradigm. There’s no client-side router to maintain, there’s no state to carefully manage. The persistent process is managed by Turbo, and you write your server-side code as though you were living back in the early aughts – blissfully isolated from the complexities of today’s SPA monstrosities! -

- -

- This happens by intercepting all clicks on links to the same domain. When you click an eligible link, Turbo Drive prevents the browser from following it, changes the browser’s URL using the History API, requests the new page using fetch, and then renders the HTML response. -

- -

- Same deal with forms. Their submissions are turned into fetch requests from which Turbo Drive will follow the redirect and render the HTML response. -

- -

- During rendering, Turbo Drive replaces the current 'body' element outright and merges the contents of the 'head' element. The JavaScript window and document objects, and the 'html' element, persist from one rendering to the next. -

- -

- While it’s possible to interact directly with Turbo Drive to control how visits happen or hook into the lifecycle of the request, the majority of the time this is a drop-in replacement where the speed is free just by adopting a few conventions. -

- -

- Navigate with Turbo Drive -

\ No newline at end of file diff --git a/views/turbo-frames.ejs b/views/turbo-frames.ejs deleted file mode 100644 index 448a2ea..0000000 --- a/views/turbo-frames.ejs +++ /dev/null @@ -1,32 +0,0 @@ -

- - Turbo Frames: Decompose complex pages -

- -

- Most web applications present pages that contain several independent segments. For a discussion page, you might have a navigation bar on the top, a list of messages in the center, a form at the bottom to add a new message, and a sidebar with related topics. Generating this discussion page normally means generating each segment in a serialized manner, piecing them all together, then delivering the result as a single HTML response to the browser. -

- -

- With Turbo Frames, you can place those independent segments inside frame elements that can scope their navigation and be lazily loaded. Scoped navigation means all interaction within a frame, like clicking links or submitting forms, happens within that frame, keeping the rest of the page from changing or reloading. -

- -

- Turbo Frames affords you: -

- -

- Efficient caching. In the discussion page example above, the related topics sidebar needs to expire whenever a new related topic appears, but the list of messages in the center does not. When everything is just one page, the whole cache expires as soon as any of the individual segments do. With frames, each segment is cached independently, so you get longer-lived caches with fewer dependent keys. -

- -

- Parallelized execution. Each defer-loaded frame is generated by its own HTTP request/response, which means it can be handled by a separate process. This allows for parallelized execution without having to manually manage the process. A complicated composite page that takes 400ms to complete end-to-end can be broken up with frames where the initial request might only take 50ms, and each of three defer-loaded frames each take 50ms. Now the whole page is done in 100ms because the three frames each taking 50ms run concurrently rather than sequentially. -

- -

- Ready for mobile. In mobile apps, you usually can’t have big, complicated composite pages. Each segment needs a dedicated screen. With an application built using Turbo Frames, you’ve already done this work of turning the composite page into segments. These segments can then appear in native sheets and screens without alteration (since they all have independent URLs). -

- -

- Decompose with Turbo Frames -

\ No newline at end of file diff --git a/views/turbo-native.ejs b/views/turbo-native.ejs deleted file mode 100644 index e9262f5..0000000 --- a/views/turbo-native.ejs +++ /dev/null @@ -1,24 +0,0 @@ -

- - Turbo Native: Hybrid apps for iOS & Android -

- -

- Turbo Native is ideal for building hybrid apps for iOS and Android. You can use your existing server-rendered HTML to get baseline coverage of your app’s functionality in a native wrapper. Then you can spend all the time you saved on making the few screens that really benefit from high-fidelity native controls even better. -

- -

- An application like Basecamp has hundreds of screens. Rewriting every single one of those screens would be an enormous task with very little benefit. Better to reserve the native firepower for high-touch interactions that really demand the highest fidelity. Something like the “New For You” inbox in Basecamp, for example, where we use swipe controls that need to feel just right. But most pages, like the one showing a single message, wouldn’t really be any better if they were completely native. -

- -

- Going hybrid doesn’t just speed up your development process, it also gives you more freedom to upgrade your app without going through the slow and onerous app store release processes. Anything that’s done in HTML can be changed in your web application, and instantly be available to all users. No waiting for Big Tech to approve your changes, no waiting for users to upgrade. -

- -

- Turbo Native assumes you’re using the recommended development practices available for iOS and Android. This is not a framework that abstracts native APIs away or even tries to let your native code be shareable between platforms. The part that’s shareable is the HTML that’s rendered server-side. But the native controls are written in the recommended native APIs. -

- -

- Go Native on iOS & Android -

\ No newline at end of file diff --git a/views/turbo-streams.ejs b/views/turbo-streams.ejs deleted file mode 100644 index 6f8faba..0000000 --- a/views/turbo-streams.ejs +++ /dev/null @@ -1,32 +0,0 @@ -

- - Turbo Streams: Deliver live page changes -

- -

- Making partial page changes in response to asynchronous actions is how we make the application feel alive. While Turbo Frames give us such updates in response to direct interactions within a single frame, Turbo Streams let us change any part of the page in response to updates sent over a WebSocket connection, SSE or other transport. (Think an imbox that automatically updates when a new email arrives.) -

- -

- Turbo Streams introduces a 'turbo-stream' element with seven basic actions: append, prepend, replace, update, remove, before, and after. With these actions, along with the target attribute specifying the ID of the element you want to operate on, you can encode all the mutations needed to refresh the page. You can even combine several stream elements in a single stream message. Simply include the HTML you’re interested in inserting or replacing in a template tag and Turbo does the rest. -

- -

- Reuse the server-side templates: Live page changes are generated using the same server-side templates that were used to create the first-load page. -

- -

- HTML over the wire: Since all we’re sending is HTML, you don’t need any client-side JavaScript (beyond Turbo, of course) to process the update. Yes, the HTML payload might be a tad larger than a comparable JSON, but with gzip, the difference is usually negligible, and you save all the client-side effort it takes to fetch JSON and turn it into HTML. -

- -

- Simpler control flow: It’s really clear to follow what happens when messages arrive on the WebSocket, SSE or in response to form submissions. There’s no routing, event bubbling, or other indirection required. It’s just the HTML to be changed, wrapped in a single tag that tells us how. -

- -

- Now, unlike RJS and SJR, it’s not possible to call custom JavaScript functions as part of a Turbo Streams action. But this is a feature, not a bug. Those techniques can easily end up producing a tangled mess when way too much JavaScript is sent along with the response. Turbo focuses squarely on just updating the DOM, and then assumes you’ll connect any additional behavior using Stimulus actions and lifecycle callbacks. -

- -

- Come Alive with Turbo Streams -

\ No newline at end of file diff --git a/views/two.ejs b/views/two.ejs deleted file mode 100644 index a5e485f..0000000 --- a/views/two.ejs +++ /dev/null @@ -1,9 +0,0 @@ -

Push or Replace?

-

Isn’t this fun?

- -<% if (action && action == "replace") { %> -

This screen was loaded via a replace action. That replaced the "How’d You Get Here?" screen that was previously on the top of the navigation stack. Now, going back will take you to the home screen.

-<% } else { %> -

This screen was pushed on the navigation stack through an advance action. You can go back to the previous screen by tapping the Back button.

-

Go back and try the replace link to see the difference.

-<% } %>