├── log
└── .keep
├── storage
└── .keep
├── tmp
├── .keep
├── pids
│ └── .keep
└── storage
│ └── .keep
├── vendor
├── .keep
└── javascript
│ └── .keep
├── lib
├── assets
│ └── .keep
└── tasks
│ └── .keep
├── public
├── favicon.ico
├── apple-touch-icon.png
├── apple-touch-icon-precomposed.png
├── robots.txt
├── 500.html
├── 422.html
└── 404.html
├── .ruby-version
├── app
├── assets
│ ├── images
│ │ └── .keep
│ ├── config
│ │ └── manifest.js
│ └── stylesheets
│ │ ├── application.css
│ │ └── pico.css
├── models
│ ├── concerns
│ │ └── .keep
│ ├── application_record.rb
│ ├── blog.rb
│ ├── user.rb
│ └── post.rb
├── controllers
│ ├── concerns
│ │ ├── .keep
│ │ ├── batchable.rb
│ │ ├── resource.rb
│ │ └── assignable.rb
│ ├── sessions_controller.rb
│ ├── users
│ │ ├── sessions_controller.rb
│ │ └── blogs_controller.rb
│ ├── application_controller.rb
│ ├── posts_controller.rb
│ ├── blogs
│ │ ├── posts_controller.rb
│ │ └── batch
│ │ │ └── posts_controller.rb
│ ├── users_controller.rb
│ └── blogs_controller.rb
├── views
│ ├── layouts
│ │ ├── mailer.text.erb
│ │ ├── mailer.html.erb
│ │ ├── application_layout.rb
│ │ └── page_layout.rb
│ ├── posts
│ │ ├── new.rb
│ │ ├── view.rb
│ │ ├── edit.rb
│ │ ├── form.rb
│ │ ├── show.rb
│ │ └── index.rb
│ ├── components
│ │ ├── application_component.rb
│ │ ├── list_component.rb
│ │ └── table_component.rb
│ ├── application_form.rb
│ ├── application_view.rb
│ └── superview
│ │ └── turbo.rb
├── helpers
│ ├── blogs_helper.rb
│ ├── posts_helper.rb
│ ├── users_helper.rb
│ ├── sessions_helper.rb
│ ├── application_helper.rb
│ ├── blogs
│ │ └── posts_helper.rb
│ ├── users
│ │ ├── blogs_helper.rb
│ │ └── sessions_helper.rb
│ └── link_helpers.rb
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
├── mailers
│ └── application_mailer.rb
├── javascript
│ ├── application.js
│ └── controllers
│ │ ├── hello_controller.js
│ │ ├── application.js
│ │ └── index.js
└── jobs
│ └── application_job.rb
├── .rspec
├── Procfile.dev
├── bin
├── rake
├── importmap
├── rails
├── docker-entrypoint
├── setup
└── bundle
├── config
├── dockerfile.yml
├── environment.rb
├── cable.yml
├── boot.rb
├── importmap.rb
├── initializers
│ ├── filter_parameter_logging.rb
│ ├── permissions_policy.rb
│ ├── assets.rb
│ ├── inflections.rb
│ └── content_security_policy.rb
├── credentials.yml.enc
├── routes.rb
├── locales
│ └── en.yml
├── application.rb
├── database.yml
├── storage.yml
├── puma.rb
└── environments
│ ├── test.rb
│ ├── development.rb
│ └── production.rb
├── config.ru
├── Rakefile
├── db
├── migrate
│ ├── 20230320162030_create_users.rb
│ ├── 20230320162044_create_blogs.rb
│ └── 20230320162105_create_posts.rb
├── seeds.rb
└── schema.rb
├── .gitattributes
├── fly.toml
├── .dockerignore
├── Guardfile
├── .gitignore
├── MIT-LICENSE
├── Dockerfile
├── Gemfile
├── spec
├── rails_helper.rb
├── spec_helper.rb
└── views
│ └── phlex
│ └── superform_spec.rb
├── README.md
└── Gemfile.lock
/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/pids/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tmp/storage/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 3.2.1
2 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vendor/javascript/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --require spec_helper
2 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Procfile.dev:
--------------------------------------------------------------------------------
1 | web: bin/rails server --port=$PORT
2 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/app/helpers/blogs_helper.rb:
--------------------------------------------------------------------------------
1 | module BlogsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/posts_helper.rb:
--------------------------------------------------------------------------------
1 | module PostsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/users_helper.rb:
--------------------------------------------------------------------------------
1 | module UsersHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/sessions_helper.rb:
--------------------------------------------------------------------------------
1 | module SessionsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/blogs/posts_helper.rb:
--------------------------------------------------------------------------------
1 | module Blogs::PostsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/users/blogs_helper.rb:
--------------------------------------------------------------------------------
1 | module Users::BlogsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/helpers/users/sessions_helper.rb:
--------------------------------------------------------------------------------
1 | module Users::SessionsHelper
2 | end
3 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | class ApplicationRecord < ActiveRecord::Base
2 | primary_abstract_class
3 | end
4 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/bin/importmap:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require_relative "../config/application"
4 | require "importmap/commands"
5 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 |
--------------------------------------------------------------------------------
/config/dockerfile.yml:
--------------------------------------------------------------------------------
1 | # generated by dockerfile-rails
2 |
3 | ---
4 | options:
5 | label:
6 | fly_launch_runtime: rails
7 |
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Channel < ActionCable::Channel::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | module ApplicationCable
2 | class Connection < ActionCable::Connection::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | class ApplicationMailer < ActionMailer::Base
2 | default from: "from@example.com"
3 | layout "mailer"
4 | end
5 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/app/views/posts/new.rb:
--------------------------------------------------------------------------------
1 | module Posts
2 | class New < View
3 | def title = "New Post"
4 |
5 | def template
6 | render Form.new(Post.new)
7 | end
8 | end
9 | end
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 | //= link_tree ../../javascript .js
4 | //= link_tree ../../../vendor/javascript .js
5 |
--------------------------------------------------------------------------------
/app/controllers/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class SessionsController < ApplicationController
2 | def destroy
3 | session[:user_id] = nil
4 | redirect_to root_url
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative "config/environment"
4 |
5 | run Rails.application
6 | Rails.application.load_server
7 |
--------------------------------------------------------------------------------
/app/javascript/application.js:
--------------------------------------------------------------------------------
1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2 | import "@hotwired/turbo-rails"
3 | import "controllers"
4 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: litecable
3 |
4 | test:
5 | adapter: test
6 |
7 | staging:
8 | adapter: litecable
9 |
10 | production:
11 | adapter: litecable
12 |
--------------------------------------------------------------------------------
/app/views/posts/view.rb:
--------------------------------------------------------------------------------
1 | module Posts
2 | class View < ApplicationView
3 | attr_writer :post
4 |
5 | turbo method: :morph do
6 | stream_from @post, @current_user
7 | end
8 | end
9 | end
--------------------------------------------------------------------------------
/app/models/blog.rb:
--------------------------------------------------------------------------------
1 | class Blog < ApplicationRecord
2 | belongs_to :user, required: true, touch: true
3 | has_many :posts
4 |
5 |
6 | validates :title, presence: true
7 |
8 | broadcasts_refreshes
9 | end
10 |
--------------------------------------------------------------------------------
/app/controllers/users/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class Users::SessionsController < ApplicationController
2 | def create
3 | session[:user_id] = params.require(:user).permit(:id)
4 | redirect_to root_url
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/javascript/controllers/hello_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus"
2 |
3 | export default class extends Controller {
4 | connect() {
5 | this.element.textContent = "Hello World!"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ApplicationRecord
2 | has_many :blogs
3 | has_many :posts
4 |
5 | validates :name, presence: true
6 | validates :email, presence: true
7 |
8 | broadcasts_refreshes
9 | end
10 |
--------------------------------------------------------------------------------
/bin/docker-entrypoint:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | # If running the rails server then create or migrate existing database
4 | if [ "${*}" == "./bin/rails server" ]; then
5 | ./bin/rails db:prepare db:seed
6 | fi
7 |
8 | exec "${@}"
9 |
--------------------------------------------------------------------------------
/app/views/posts/edit.rb:
--------------------------------------------------------------------------------
1 | module Posts
2 | class Edit < View
3 | def title = @post.title
4 | def subtitle = show(@post.blog, :title)
5 |
6 | def template
7 | render Form.new(@post)
8 | end
9 | end
10 | end
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
2 |
3 | require "bundler/setup" # Set up gems listed in the Gemfile.
4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations.
5 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require_relative "config/application"
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/db/migrate/20230320162030_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration[7.0]
2 | def change
3 | create_table :users do |t|
4 | t.string :name
5 | t.string :email
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files.
2 |
3 | # Mark the database schema as having been generated.
4 | db/schema.rb linguist-generated
5 |
6 | # Mark any vendored files as having been vendored.
7 | vendor/* linguist-vendored
8 |
--------------------------------------------------------------------------------
/db/migrate/20230320162044_create_blogs.rb:
--------------------------------------------------------------------------------
1 | class CreateBlogs < ActiveRecord::Migration[7.0]
2 | def change
3 | create_table :blogs do |t|
4 | t.string :title
5 | t.references :user, null: false, foreign_key: true
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/app/javascript/controllers/application.js:
--------------------------------------------------------------------------------
1 | import { Application } from "@hotwired/stimulus"
2 |
3 | const application = Application.start()
4 |
5 | // Configure Stimulus development experience
6 | application.debug = false
7 | window.Stimulus = application
8 |
9 | export { application }
10 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | class ApplicationJob < ActiveJob::Base
2 | # Automatically retry jobs that encountered a deadlock
3 | # retry_on ActiveRecord::Deadlocked
4 |
5 | # Most jobs are safe to ignore if the underlying records are no longer available
6 | # discard_on ActiveJob::DeserializationError
7 | end
8 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | include Assignable
3 | include Superview::Actions
4 | include Resource
5 |
6 | layout false
7 |
8 | def current_user
9 | @current_user ||= User.find_or_create_by!(email: "somebody@example.com", name: "Somebody")
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/config/importmap.rb:
--------------------------------------------------------------------------------
1 | # Pin npm packages by running ./bin/importmap
2 |
3 | pin "application", preload: true
4 | pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
5 | pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
6 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
7 | pin_all_from "app/javascript/controllers", under: "controllers"
8 |
--------------------------------------------------------------------------------
/db/migrate/20230320162105_create_posts.rb:
--------------------------------------------------------------------------------
1 | class CreatePosts < ActiveRecord::Migration[7.0]
2 | def change
3 | create_table :posts do |t|
4 | t.string :title
5 | t.text :content
6 | t.datetime :publish_at
7 |
8 | t.references :user, null: false, foreign_key: true
9 | t.references :blog, null: false, foreign_key: true
10 |
11 | t.timestamps
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of
4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
5 | # notations and behaviors.
6 | Rails.application.config.filter_parameters += [
7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
8 | ]
9 |
--------------------------------------------------------------------------------
/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Define an application-wide HTTP permissions policy. For further
2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy
3 | #
4 | # Rails.application.config.permissions_policy do |f|
5 | # f.camera :none
6 | # f.gyroscope :none
7 | # f.microphone :none
8 | # f.usb :none
9 | # f.fullscreen :self
10 | # f.payment :self, "https://secure.example.com"
11 | # end
12 |
--------------------------------------------------------------------------------
/app/views/components/application_component.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationComponent < Phlex::HTML
4 | include Phlex::Rails::Helpers::Routes
5 | include LinkHelpers
6 | include Superview::Turbo::Helpers
7 |
8 | if Rails.env.development?
9 | def before_template
10 | comment { "Before #{self.class.name}" }
11 | end
12 | end
13 |
14 | def after_template
15 | turbo if respond_to? :turbo
16 | super
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/app/views/components/list_component.rb:
--------------------------------------------------------------------------------
1 | class ListComponent < ApplicationComponent
2 | include Phlex::DeferredRender
3 |
4 | def initialize(items:)
5 | @items = items
6 | end
7 |
8 | def template(&)
9 | @items.each do |item|
10 | li { @item_template.call(item) }
11 | end
12 | end
13 |
14 | def item(&item_template)
15 | @item_template = item_template
16 | end
17 |
18 | def around_template(&)
19 | ol { super }
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | PlulxZUW/VB5E5mv8EnHPC/ugiLnAQdu9H61uEi4GvGbT7mNMjZsSqEagUsq64iuOkC0HcDw0KjRFFrFmKXOOc1u4/xRT37w3xelLUOcOPe+08y3YiG1Xpz+fEeMrwdn0iGsv/YkIKBPBjg3ZlBqddTrS5yXEr5ZFdj20IQ13LHJ8a+802bXMd4lV7HfvoDJx3rR7FBQ9nPRSMTBL6Qy6ADH9bW5WkhJGhLy2PoV8vxswRcHQ+9GDBGvFQQrLVX6ElDIRO3IMF7Qe6Wr7Ytyjo1uOLUP47sDTQNMs+bMYB3vj+K2xs2mM8GBGo8Y85j3/0i+LTND5cZH3NGnk77QfpsU31gjj4L2naG1Tc2yiKHKDUCb/vXBL02n+odK6gmr8aq1Aog5i3IkC/nGC3U/Dus5Y8XmqS+YbwbK--GVDB133IEzqOLjyk--9AhQfCNISfjwFj/kxym+kw==
--------------------------------------------------------------------------------
/app/controllers/posts_controller.rb:
--------------------------------------------------------------------------------
1 | class PostsController < ApplicationController
2 | resources :posts, from: :current_user
3 |
4 | # Demonstration of including views that have been moved
5 | # from the controller into their own files. This makes sense
6 | # if your views need to be shared between multiple controllers
7 | # or if your project prefers to have views as their own files.
8 | include Posts
9 |
10 | private
11 |
12 | def destroyed_url
13 | @post.blog
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/models/post.rb:
--------------------------------------------------------------------------------
1 | class Post < ApplicationRecord
2 | belongs_to :user, touch: true
3 | belongs_to :blog, touch: true
4 |
5 | validates :title, presence: true
6 |
7 | attribute :publish_at, Inputomatic::DateTime.new
8 |
9 | broadcasts_refreshes
10 |
11 | def status
12 | status = if publish_at.nil?
13 | "Draft"
14 | elsif publish_at > Time.current
15 | "Scheduled"
16 | else
17 | "Published"
18 | end
19 |
20 | ActiveSupport::StringInquirer.new(status)
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/app/views/posts/form.rb:
--------------------------------------------------------------------------------
1 | module Posts
2 | class Form < ApplicationForm
3 | def template
4 | labeled field(:blog_id).select Blog.select(:id, :title), nil
5 | # Same thing as above, but multiple lines. Useful for optgroups.
6 | # labeled field(:blog).select do
7 | # _1.options(Blog.select(:id, :title))
8 | # _1.blank_option
9 | # end
10 |
11 | labeled field(:title).input.focus
12 | labeled field(:publish_at).input
13 | labeled field(:content).textarea(rows: 6)
14 |
15 | submit
16 | end
17 | end
18 | end
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = "1.0"
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in the app/assets
11 | # folder are already added.
12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
13 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | def batch(*actions, **kwargs)
2 | collection do
3 | namespace :batch do
4 | get :index
5 | post :update
6 | actions.each do |action|
7 | post action
8 | end
9 | end
10 | end
11 | end
12 |
13 | Rails.application.routes.draw do
14 | resources :users, except: :destroy do
15 | nest :blogs
16 | create :session
17 | end
18 |
19 | resources :blogs do
20 | nest :posts do
21 | batch :delete, :publish, :unpublish
22 | end
23 | end
24 |
25 | resources :posts
26 |
27 | resources :sessions
28 |
29 | root to: "blogs#index"
30 | end
31 |
--------------------------------------------------------------------------------
/app/javascript/controllers/index.js:
--------------------------------------------------------------------------------
1 | // Import and register all your controllers from the importmap under controllers/*
2 |
3 | import { application } from "controllers/application"
4 |
5 | // Eager load all controllers defined in the import map under controllers/**/*_controller
6 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
7 | eagerLoadControllersFrom("controllers", application)
8 |
9 | // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
10 | // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
11 | // lazyLoadControllersFrom("controllers", application)
12 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }])
7 | # Character.create(name: "Luke", movie: movies.first)
8 |
9 | User.create!(name: "Bob Law", email: "bob.law@example.com").tap do |user|
10 | user.blogs.create!(title: "Law Blog").tap do |blog|
11 | (1..10).each do |n|
12 | blog.posts.create! title: "The #{n.ordinalize} Post", user: user
13 | end
14 | end
15 | end
--------------------------------------------------------------------------------
/app/controllers/blogs/posts_controller.rb:
--------------------------------------------------------------------------------
1 | class Blogs::PostsController < ApplicationController
2 | resources :posts, through: :blogs, from: :current_user
3 | before_action :assign_user, only: %i[new create]
4 |
5 | class New < ApplicationView
6 | attr_accessor :current_user, :blog, :post
7 | turbo method: :morph do
8 | stream_from @blog, @blog.user
9 | end
10 |
11 | def title = "Create a new post"
12 | def subtitle = show(@blog, :title)
13 |
14 | def template(&)
15 | render PostsController::Form.new(@post)
16 | end
17 | end
18 |
19 | private
20 |
21 | def assign_user
22 | @post.user = current_user
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/views/layouts/application_layout.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationLayout < ApplicationComponent
4 | include Phlex::Rails::Layout
5 |
6 | def initialize(title:, turbo:)
7 | @title = title
8 | @turbo = turbo
9 | end
10 |
11 | def template(&)
12 | doctype
13 |
14 | html do
15 | head do
16 | title(&@title)
17 | meta name: "viewport", content: "width=device-width,initial-scale=1"
18 | csp_meta_tag
19 | csrf_meta_tags
20 | stylesheet_link_tag "application", data_turbo_track: "reload"
21 | javascript_importmap_tags
22 | render @turbo
23 | end
24 |
25 | body(&)
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, "\\1en"
8 | # inflect.singular /^(ox)en/i, "\\1"
9 | # inflect.irregular "person", "people"
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | inflect.acronym "DOM"
16 | end
17 |
--------------------------------------------------------------------------------
/app/views/posts/show.rb:
--------------------------------------------------------------------------------
1 | module Posts
2 | class Show < View
3 | def title = @post.title
4 | def subtitle = show(@post.blog, :title)
5 |
6 | def template
7 | table do
8 | tbody do
9 | tr do
10 | th { "Status" }
11 | td { @post.status }
12 | end
13 | tr do
14 | th { "Publish at" }
15 | td { @post.publish_at&.to_formatted_s(:long) }
16 | end
17 | tr do
18 | th { "Content" }
19 | td do
20 | article { @post.content }
21 | end
22 | end
23 | end
24 | end
25 | nav do
26 | edit(@post, role: "button")
27 | delete(@post)
28 | end
29 | end
30 | end
31 | end
--------------------------------------------------------------------------------
/app/controllers/users/blogs_controller.rb:
--------------------------------------------------------------------------------
1 | class Users::BlogsController < ApplicationController
2 | resources :blogs, from: :current_user
3 |
4 | class New < ApplicationView
5 | attr_writer :blog
6 |
7 | def title = "Create blog"
8 | def subtitle = "You'll be writing awesome stuff in no time"
9 |
10 | def template
11 | render BlogsController::Form.new(@blog)
12 | end
13 | end
14 |
15 | class Index < ApplicationView
16 | attr_writer :blogs, :current_user
17 |
18 | def title = "#{@current_user.name}'s Blogs"
19 |
20 | def template(&)
21 | list(@blogs) do |blog|
22 | show(blog, :title)
23 | end
24 | nav do
25 | create(@current_user.blogs, role: "button")
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml app configuration file generated for oxidizer-demo on 2023-06-07T16:35:27-07:00
2 | #
3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4 | #
5 |
6 | app = "oxidizer-demo"
7 | primary_region = "sjc"
8 | kill_signal = "SIGINT"
9 | kill_timeout = "5s"
10 |
11 | [experimental]
12 | auto_rollback = true
13 |
14 | [env]
15 | PRIMARY_REGION = "sjc"
16 |
17 | [http_service]
18 | internal_port = 3000
19 | force_https = true
20 | auto_stop_machines = true
21 | auto_start_machines = true
22 | min_machines_running = 1
23 | processes = ["app"]
24 |
25 | [[statics]]
26 | guest_path = "/rails/public"
27 | url_prefix = "/"
28 |
29 | [mounts]
30 | source="oxidizer_demo_litestack_data"
31 | destination="/data"
--------------------------------------------------------------------------------
/app/views/application_form.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationForm < Superform::Rails::Form
4 | class LabelField < Superform::Rails::Components::LabelComponent
5 | def template(&content)
6 | content ||= Proc.new { field.title }
7 | label(**attributes) { strong(&content) }
8 | end
9 | end
10 |
11 | class Field < Field
12 | def label(**attributes)
13 | LabelField.new(self, attributes: attributes)
14 | end
15 | end
16 |
17 | # def field(name)
18 | # if reflection = @model.class.reflect_on_association(name)
19 | # name = reflection.foreign_key
20 | # end
21 | # super(name)
22 | # end
23 |
24 | def labeled(component, &)
25 | render component.field.label
26 | render component, &
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/views/posts/index.rb:
--------------------------------------------------------------------------------
1 | module Posts
2 | class Index < ApplicationView
3 | attr_writer :posts, :current_user
4 |
5 | turbo method: :morph do
6 | stream_from @posts, @current_user
7 | end
8 |
9 | def title = "#{@current_user.name}'s Posts"
10 |
11 | def template
12 | render TableComponent.new(items: @posts) do |table|
13 | table.column("Title") { show(_1, :title) }
14 | table.column do |column|
15 | # Titles might not always be text, so we need to handle rendering
16 | # Phlex markup within.
17 | column.title do
18 | link_to(user_blogs_path(@current_user)) { "Blogs" }
19 | end
20 | column.item { show(_1.blog, :title) }
21 | end
22 | end
23 | end
24 | end
25 | end
--------------------------------------------------------------------------------
/app/views/application_view.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationView < ApplicationComponent
4 | include LinkHelpers
5 |
6 | attr_writer :resource, :resources
7 | attr_reader :forms
8 |
9 | def title = nil
10 | def subtitle = nil
11 |
12 | def list(collection, &item_template)
13 | render ListComponent.new(items: collection) do |list|
14 | list.item(&item_template)
15 | end
16 | end
17 |
18 | def initialize(...)
19 | @forms = []
20 | @turbo_meta_tags = self.class.turbo_meta_tags
21 | super(...)
22 | end
23 |
24 | def render(view, ...)
25 | @forms.push view if view.is_a? ApplicationForm
26 | super(view, ...)
27 | end
28 |
29 | def around_template(&)
30 | render PageLayout.new(title: proc { title }, subtitle: proc { subtitle }, turbo: @turbo_meta_tags) do
31 | super(&)
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | # See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files.
2 |
3 | # Ignore git directory.
4 | /.git/
5 |
6 | # Ignore bundler config.
7 | /.bundle
8 |
9 | # Ignore all default key files.
10 | /config/master.key
11 | /config/credentials/*.key
12 |
13 | # Ignore all environment files.
14 | /.env*
15 | !/.env.example
16 |
17 | # Ignore all logfiles and tempfiles.
18 | /log/*
19 | /tmp/*
20 | !/log/.keep
21 | !/tmp/.keep
22 |
23 | # Ignore pidfiles, but keep the directory.
24 | /tmp/pids/*
25 | !/tmp/pids/
26 | !/tmp/pids/.keep
27 |
28 | # Ignore storage (uploaded files in development and any SQLite databases).
29 | /storage/*
30 | !/storage/.keep
31 | /tmp/storage/*
32 | !/tmp/storage/
33 | !/tmp/storage/.keep
34 |
35 | # Ignore assets.
36 | /node_modules/
37 | /app/assets/builds/*
38 | !/app/assets/builds/.keep
39 | /public/assets
40 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | # A sample Guardfile
2 | # More info at https://github.com/guard/guard#readme
3 |
4 | ## Uncomment and set this to only include directories you want to watch
5 | # directories %w(app lib config test spec features) \
6 | # .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")}
7 |
8 | ## Note: if you are using the `directories` clause above and you are not
9 | ## watching the project directory ('.'), then you will want to move
10 | ## the Guardfile to a watched dir and symlink it back, e.g.
11 | #
12 | # $ mkdir config
13 | # $ mv Guardfile config/
14 | # $ ln -s config/Guardfile .
15 | #
16 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
17 |
18 | # Watches all Ruby files in a project.
19 | #
20 | # The `require:` parameter is configured for a Rails project. If using it for
21 | # a gem, change the require path to whatever loads a Zeitwerk::Autoloader.
22 | guard :zeitwerk, require: "./config/application.rb" do
23 | watch(/.*\.rb$/)
24 | end
25 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t "hello"
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t("hello") %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # "true": "foo"
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-*
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | /tmp/*
17 | !/log/.keep
18 | !/tmp/.keep
19 |
20 | # Ignore pidfiles, but keep the directory.
21 | /tmp/pids/*
22 | !/tmp/pids/
23 | !/tmp/pids/.keep
24 |
25 | # Ignore uploaded files in development.
26 | /storage/*
27 | !/storage/.keep
28 | /tmp/storage/*
29 | !/tmp/storage/
30 | !/tmp/storage/.keep
31 |
32 | /public/assets
33 |
34 | # Ignore master key for decrypting credentials and more.
35 | /config/master.key
36 |
37 | # Ignore default Litestack SQLite databases.
38 | /db/**/*.sqlite3
39 | /db/**/*.sqlite3-*
40 |
41 | .env
--------------------------------------------------------------------------------
/app/views/layouts/page_layout.rb:
--------------------------------------------------------------------------------
1 | class PageLayout < ApplicationLayout
2 | def initialize(subtitle: nil, **kwargs)
3 | super **kwargs
4 | @subtitle = subtitle
5 | end
6 |
7 | def template(&)
8 | super do
9 | header(class: "container") do
10 | if @title and @subtitle
11 | hgroup do
12 | h1(&@title)
13 | h2(&@subtitle)
14 | end
15 | else
16 | h1 { @title }
17 | end
18 | end
19 | main(class: "container", &)
20 | footer(class: "container") do
21 | small do
22 | plain "This demo application is erased on every deploy. Read more about it at "
23 | link_to("https://fly.io/ruby-dispatch/component-driven-development-on-rails-with-phlex") do
24 | "Component Driven Development"
25 | end
26 | plain " or view the "
27 | link_to("https://github.com/rubymonolith/demo") { "source code on Github" }
28 | plain "."
29 | end
30 | end
31 | end
32 | end
33 | end
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails/all"
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module Resourcerer
10 | class Application < Rails::Application
11 | config.autoload_paths << "#{root}/app/views"
12 | config.autoload_paths << "#{root}/app/views/layouts"
13 | config.autoload_paths << "#{root}/app/views/components"
14 | config.autoload_paths << "#{root}/lib/generators"
15 |
16 | # Initialize configuration defaults for originally generated Rails version.
17 | config.load_defaults 7.0
18 |
19 | # Configuration for the application, engines, and railties goes here.
20 | #
21 | # These settings can be overridden in specific environments using the files
22 | # in config/environments, which are processed later.
23 | #
24 | # config.time_zone = "Central Time (US & Canada)"
25 | # config.eager_load_paths << Rails.root.join("extras")
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite. Versions 3.8.0 and up are supported.
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem "sqlite3"
6 | #
7 | # `Litesupport.root.join("data.sqlite3")` stores
8 | # application data in the path `./db/#{Rails.env}/data.sqlite3`
9 | default: &default
10 | adapter: litedb
11 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
12 | timeout: 5000
13 | database: <%= Litesupport.root.join("data.sqlite3") %>
14 |
15 | development:
16 | <<: *default
17 |
18 | # Warning: The database defined as "test" will be erased and
19 | # re-generated from your development database when you run "rake".
20 | # Do not set this db to the same as development or production.
21 | test:
22 | <<: *default
23 |
24 | # Warning: Make sure your production database path is on a persistent
25 | # volume, otherwise your application data could be deleted between deploys.
26 | #
27 | # You may also set the Litesupport.root in production via the `LITESTACK_DATA_PATH`
28 | # environment variable.
29 | production:
30 | <<: *default
31 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2022 Brad Gessler
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?("config/database.yml")
22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 | end
34 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy.
4 | # See the Securing Rails Applications Guide for more information:
5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header
6 |
7 | # Rails.application.configure do
8 | # config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 | #
19 | # # Generate session nonces for permitted importmap and inline scripts
20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21 | # config.content_security_policy_nonce_directives = %w(script-src)
22 | #
23 | # # Report violations without enforcing the policy.
24 | # # config.content_security_policy_report_only = true
25 | # end
26 |
--------------------------------------------------------------------------------
/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket-<%= Rails.env %>
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket-<%= Rails.env %>
23 |
24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name-<%= Rails.env %>
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/app/views/components/table_component.rb:
--------------------------------------------------------------------------------
1 | class TableComponent < ApplicationComponent
2 | include Phlex::DeferredRender
3 |
4 | class Column
5 | attr_accessor :title_template, :item_template
6 |
7 | def title(&block)
8 | @title_template = block
9 | end
10 |
11 | def item(&block)
12 | @item_template = block
13 | end
14 |
15 | def self.build(title:, &block)
16 | new.tap do |column|
17 | column.title { title }
18 | column.item(&block)
19 | end
20 | end
21 | end
22 |
23 | def initialize(items: [])
24 | @items = items
25 | @columns = []
26 | end
27 |
28 | def template(&)
29 | table do
30 | thead do
31 | tr do
32 | @columns.each do |column|
33 | th(&column.title_template)
34 | end
35 | end
36 | end
37 | tbody do
38 | @items.each do |item|
39 | tr do
40 | @columns.each do |column|
41 | td { column.item_template.call(item) }
42 | end
43 | end
44 | end
45 | end
46 | end
47 | end
48 |
49 | def column(title = nil, &block)
50 | @columns << if title
51 | Column.build(title: title, &block)
52 | else
53 | Column.new.tap(&block)
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | resources :users
3 |
4 | class Form < ApplicationForm
5 | def template(&)
6 | labeled field(:name).input
7 | labeled field(:email).input
8 | submit
9 | end
10 | end
11 |
12 | class Index < ApplicationView
13 | attr_writer :users
14 |
15 | def title = "People"
16 | def subtitle = "Create users and sign in"
17 |
18 | def template(&)
19 | section do
20 | ul do
21 | @users.each do |user|
22 | li { show(user, :name) }
23 | end
24 | end
25 | create(@users, role: "button")
26 | end
27 | end
28 | end
29 |
30 | class Show < ApplicationView
31 | attr_writer :user
32 |
33 | def title = @user.name
34 | def subtitle = @user.email
35 |
36 | def template(&)
37 | list(@user.blogs) do |blog|
38 | show(blog, :title)
39 | end
40 | nav do
41 | create(@user.blogs, role: "button")
42 | edit(@user, role: "secondary")
43 | end
44 | end
45 | end
46 |
47 | class New < ApplicationView
48 | attr_writer :user
49 |
50 | def title = "Create user"
51 |
52 | def template(&)
53 | render Form.new(@user)
54 | end
55 | end
56 |
57 | class Edit < ApplicationView
58 | attr_writer :user
59 |
60 | def title = "Edit #{@user.name}"
61 |
62 | def template(&)
63 | render Form.new(@user)
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/app/helpers/link_helpers.rb:
--------------------------------------------------------------------------------
1 | module LinkHelpers
2 | # Give us some sane link helpers to work with in Phlex. They kind
3 | # of mimic Rails helpers, but are "Phlexable".
4 | def link_to(target, **attributes, &)
5 | a(href: url_for(target), **attributes, &)
6 | end
7 |
8 | def show(model, attribute = nil, *args, **kwargs, &content)
9 | content ||= Proc.new { model.send(attribute) } if attribute
10 | link_to(model, *args, **kwargs, &content)
11 | end
12 |
13 | def edit(model, *args, **kwargs, &content)
14 | content ||= Proc.new { "Edit #{model.class.model_name}" }
15 | link_to([:edit, model], *args, **kwargs, &content)
16 | end
17 |
18 | def delete(model, *args, data: {}, **kwargs, &content)
19 | content ||= Proc.new { "Delete #{model.class.model_name}" }
20 | link_to(model, *args, data: data.merge("turbo-method": :delete), **kwargs, &content)
21 | end
22 |
23 | def create(scope = nil, *args, **kwargs, &content)
24 | target = if scope.respond_to? :proxy_association
25 | owner = scope.proxy_association.owner
26 | model = scope.proxy_association.reflection.klass.model_name
27 | element = scope.proxy_association.reflection.klass.model_name.element.to_sym
28 | [:new, owner, element]
29 | elsif scope.respond_to? :model
30 | model = scope.model
31 | [:new, model.model_name.element.to_sym]
32 | else
33 | model = scope
34 | [:new, scope]
35 | end
36 |
37 | content ||= Proc.new { "Create #{model}" }
38 |
39 | link_to(target, *args, **kwargs, &content)
40 | end
41 | end
--------------------------------------------------------------------------------
/app/controllers/concerns/batchable.rb:
--------------------------------------------------------------------------------
1 | module Batchable
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | before_action :assign_selection
6 | end
7 |
8 | class Selection
9 | include ActiveModel::API
10 | attr_accessor :selected, :action, :scope
11 |
12 | def selected
13 | @selected ||= []
14 | end
15 |
16 | def selected?(item = nil)
17 | if item
18 | selected.include? item.to_s
19 | else
20 | selected.any?
21 | end
22 | end
23 |
24 | def select_none
25 | self.selected = []
26 | end
27 |
28 | def select_all
29 | self.selected = items.pluck(:id).map(&:to_s)
30 | end
31 |
32 | def selected_items
33 | items.where(id: selected)
34 | end
35 |
36 | def items
37 | @scope
38 | end
39 |
40 | def self.action_param_key
41 | :action
42 | end
43 |
44 | def self.permit(params)
45 | params.fetch(model_name.param_key, {}).permit(:action, selected: [])
46 | end
47 |
48 | def self.action(params)
49 | params.dig model_name.param_key, action_param_key
50 | end
51 | end
52 |
53 | protected
54 |
55 | def assign_selection
56 | @selection = Selection.new(scope: scope, **permitted_batch_params)
57 | end
58 |
59 | def permitted_batch_params
60 | Selection.permit params
61 | end
62 |
63 | def method_for_action(action_name)
64 | routable_batch_action? ? Selection.action(params) : super
65 | end
66 |
67 | def routable_batch_action?
68 | self.class.action_methods.include? Selection.action(params)
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # This file is the source Rails uses to define your schema when running `bin/rails
6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7 | # be faster and is potentially less error prone than running all of your
8 | # migrations from scratch. Old migrations may fail to apply correctly if those
9 | # migrations use external dependencies or application code.
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema[7.1].define(version: 2023_03_20_162105) do
14 | create_table "blogs", force: :cascade do |t|
15 | t.string "title"
16 | t.integer "user_id", null: false
17 | t.datetime "created_at", null: false
18 | t.datetime "updated_at", null: false
19 | t.index ["user_id"], name: "index_blogs_on_user_id"
20 | end
21 |
22 | create_table "posts", force: :cascade do |t|
23 | t.string "title"
24 | t.text "content"
25 | t.datetime "publish_at"
26 | t.integer "user_id", null: false
27 | t.integer "blog_id", null: false
28 | t.datetime "created_at", null: false
29 | t.datetime "updated_at", null: false
30 | t.index ["blog_id"], name: "index_posts_on_blog_id"
31 | t.index ["user_id"], name: "index_posts_on_user_id"
32 | end
33 |
34 | create_table "users", force: :cascade do |t|
35 | t.string "name"
36 | t.string "email"
37 | t.datetime "created_at", null: false
38 | t.datetime "updated_at", null: false
39 | end
40 |
41 | add_foreign_key "blogs", "users"
42 | add_foreign_key "posts", "blogs"
43 | add_foreign_key "posts", "users"
44 | end
45 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
12 | # terminating a worker in development environments.
13 | #
14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
15 |
16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
17 | #
18 | port ENV.fetch("PORT") { 3000 }
19 |
20 | # Specifies the `environment` that Puma will run in.
21 | #
22 | environment ENV.fetch("RAILS_ENV") { "development" }
23 |
24 | # Specifies the `pidfile` that Puma will use.
25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
26 |
27 | # Specifies the number of `workers` to boot in clustered mode.
28 | # Workers are forked web server processes. If using threads and workers together
29 | # the concurrency of the application would be max `threads` * `workers`.
30 | # Workers do not work on JRuby or Windows (both of which do not support
31 | # processes).
32 | #
33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
34 |
35 | # Use the `preload_app!` method when specifying a `workers` number.
36 | # This directive tells Puma to first boot the application and load code
37 | # before forking the application. This takes advantage of Copy On Write
38 | # process behavior so workers use less memory.
39 | #
40 | # preload_app!
41 |
42 | # Allow puma to be restarted by `bin/rails restart` command.
43 | plugin :tmp_restart
44 |
--------------------------------------------------------------------------------
/app/controllers/blogs_controller.rb:
--------------------------------------------------------------------------------
1 | class BlogsController < ApplicationController
2 | resources :blogs, from: :current_user
3 |
4 | class Form < ApplicationForm
5 | def template
6 | labeled field(:title).input
7 |
8 | submit
9 | end
10 | end
11 |
12 | class Show < ApplicationView
13 | attr_writer :blog, :current_user
14 | turbo method: :morph do
15 | stream_from @blog, @current_user, @blog.posts
16 | end
17 |
18 | def title = @blog.title
19 | def subtitle
20 | plain "Owned and operated by "
21 | show(@blog.user, :name)
22 | end
23 |
24 | def template(&)
25 | render TableComponent.new(items: @blog.posts) do |table|
26 | table.column("Title") { show(_1, :title) }
27 | table.column("Author") { show(_1.user, :name) }
28 | table.column("Status") { "Not Published" }
29 | table.column("Publish Date") { _1.publish_at&.to_formatted_s(:long) }
30 | end
31 | nav do
32 | create(@blog.posts, role: "button")
33 | show(blog_batch_posts_path(@blog)){ "Select Posts" }
34 | edit(@blog)
35 | delete(@blog)
36 | end
37 | end
38 | end
39 |
40 | class Edit < Show
41 | def template
42 | render Form.new(@blog)
43 | end
44 | end
45 |
46 | class Index < ApplicationView
47 | attr_accessor :blogs, :current_user
48 |
49 | turbo method: :morph do
50 | stream_from @current_user, @blogs
51 | end
52 |
53 | def title = "Blogs"
54 | def subtitle = "Looks like #{helpers.pluralize @blogs.count, "blog"} have been created"
55 |
56 | def template
57 | render TableComponent.new(items: @blogs) do |table|
58 | table.column("Title") { show(_1, :title) }
59 | table.column("Owner") { show(_1.user, :name) }
60 | end
61 | create(@current_user.blogs, role: "button")
62 | end
63 | end
64 |
65 | def created_url
66 | @blog
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This is a manifest file that'll be compiled into application.css, which will include all the files
3 | * listed below.
4 | *
5 | * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
6 | * vendor/assets/stylesheets directory can be referenced here using a relative path.
7 | *
8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS
10 | * files in this directory. Styles in this file should be added after the last require_* statement.
11 | * It is generally better to create a new file per style scope.
12 | *
13 | *= require_tree .
14 | *= require_self
15 | */
16 |
17 | /* Purple Light scheme (Default) */
18 | /* Can be forced with data-theme="light" */
19 | [data-theme="light"],
20 | :root:not([data-theme="dark"]) {
21 | --primary: #8e24aa;
22 | --primary-hover: #7b1fa2;
23 | --primary-focus: rgba(142, 36, 170, 0.125);
24 | --primary-inverse: #FFF;
25 | }
26 |
27 | /* Purple Dark scheme (Auto) */
28 | /* Automatically enabled if user has Dark mode enabled */
29 | @media only screen and (prefers-color-scheme: dark) {
30 | :root:not([data-theme]) {
31 | --primary: #8e24aa;
32 | --primary-hover: #9c27b0;
33 | --primary-focus: rgba(142, 36, 170, 0.25);
34 | --primary-inverse: #FFF;
35 | }
36 | }
37 |
38 | /* Purple Dark scheme (Forced) */
39 | /* Enabled if forced with data-theme="dark" */
40 | [data-theme="dark"] {
41 | --primary: #8e24aa;
42 | --primary-hover: #9c27b0;
43 | --primary-focus: rgba(142, 36, 170, 0.25);
44 | --primary-inverse: #FFF;
45 | }
46 |
47 | /* Purple (Common styles) */
48 | :root {
49 | --form-element-active-border-color: var(--primary);
50 | --form-element-focus-color: var(--primary-focus);
51 | --switch-color: var(--primary-inverse);
52 | --switch-checked-background-color: var(--primary);
53 | }
54 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/controllers/concerns/resource.rb:
--------------------------------------------------------------------------------
1 | module Resource
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | class_attribute :resource_name
6 | before_action :assign_resource_instance_variables, if: :is_resource?
7 | end
8 |
9 | class_methods do
10 | def resources(name, ...)
11 | self.resource_name = name.to_s.singularize
12 | assign(name, ...)
13 | end
14 | end
15 |
16 | def create
17 | unrendered_phlex_action(:new).forms.first.assign params.require(resource_name).permit!
18 |
19 | if @resource.save!
20 | redirect_to @resource
21 | else
22 | # Maybe I should just render it with a string? That way
23 | # if there's an error I can just show the darn thing; if there's
24 | # not an error, then I can simply redirect. I'd somehow have to
25 | # deferr rendering.
26 | render phlex_action(:new), status: :unprocessable_entity
27 | end
28 | end
29 |
30 | def update
31 | unrendered_phlex_action(:edit).forms.first.assign params.require(resource_name).permit!
32 |
33 | if @resource.save
34 | redirect_to @resource
35 | else
36 | # Maybe I should just render it with a string? That way
37 | # if there's an error I can just show the darn thing; if there's
38 | # not an error, then I can simply redirect. I'd somehow have to
39 | # deferr rendering.
40 | render phlex_action(:edit), status: :unprocessable_entity
41 | end
42 | end
43 |
44 | def destroy
45 | @resource.destroy
46 | redirect_to destroyed_url
47 | end
48 |
49 | protected
50 |
51 | def destroyed_url
52 | url_for(action: :index)
53 | end
54 |
55 | def created_url
56 | @resource
57 | end
58 | alias :updated_url :created_url
59 |
60 | private
61 |
62 | def unrendered_phlex_action(action)
63 | phlex_action(action).tap do |view|
64 | view.call(Phlex::BlackHole, view_context: view_context)
65 | end
66 | end
67 |
68 | def resource_forms(action)
69 | unrendered_phlex_action(action).forms
70 | end
71 |
72 | def assign_resource_instance_variables
73 | @resource = instance_variable_get("@#{resource_name.singularize}")
74 | @resources = instance_variable_get("@#{resource_name.pluralize}")
75 | end
76 |
77 | def is_resource?
78 | self.class.resource_name.present?
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax = docker/dockerfile:1
2 |
3 | # Make sure RUBY_VERSION matches the Ruby version in .ruby-version and Gemfile
4 | ARG RUBY_VERSION=3.2.1
5 | FROM ruby:$RUBY_VERSION-slim as base
6 |
7 | LABEL fly_launch_runtime="rails"
8 |
9 | # Rails app lives here
10 | WORKDIR /rails
11 |
12 | # Set production environment
13 | ENV RAILS_ENV="production" \
14 | BUNDLE_WITHOUT="development:test" \
15 | BUNDLE_DEPLOYMENT="1"
16 |
17 | # Update gems and bundler
18 | RUN gem update --system --no-document && \
19 | gem install -N bundler
20 |
21 |
22 | # Throw-away build stage to reduce size of final image
23 | FROM base as build
24 |
25 | # Install packages needed to build gems
26 | RUN apt-get update -qq && \
27 | apt-get install --no-install-recommends -y build-essential git pkg-config
28 |
29 | # Install application gems
30 | COPY --link Gemfile Gemfile.lock ./
31 | RUN bundle install && \
32 | bundle exec bootsnap precompile --gemfile && \
33 | rm -rf ~/.bundle/ $BUNDLE_PATH/ruby/*/cache $BUNDLE_PATH/ruby/*/bundler/gems/*/.git
34 |
35 | # Copy application code
36 | COPY --link . .
37 |
38 | # Precompile bootsnap code for faster boot times
39 | RUN bundle exec bootsnap precompile app/ lib/
40 |
41 | # Precompiling assets for production without requiring secret RAILS_MASTER_KEY
42 | RUN SECRET_KEY_BASE=DUMMY ./bin/rails assets:precompile
43 |
44 | # Final stage for app image
45 | FROM base
46 |
47 | # Install packages needed for deployment
48 | RUN apt-get update -qq && \
49 | apt-get install --no-install-recommends -y libsqlite3-0 && \
50 | rm -rf /var/lib/apt/lists /var/cache/apt/archives
51 |
52 | # Run and own the application files as a non-root user for security
53 | RUN useradd rails --home /rails --shell /bin/bash
54 | USER rails:rails
55 |
56 | # Copy built artifacts: gems, application
57 | COPY --from=build /usr/local/bundle /usr/local/bundle
58 | COPY --from=build --chown=rails:rails /rails /rails
59 |
60 | # Sqlite data will be stored in data.
61 | VOLUME /data
62 |
63 | # Deployment options
64 | ENV RAILS_LOG_TO_STDOUT="1" \
65 | RAILS_SERVE_STATIC_FILES="true" \
66 | LITESTACK_DATA_PATH="/data"
67 |
68 | # Entrypoint prepares the database.
69 | ENTRYPOINT ["/rails/bin/docker-entrypoint"]
70 |
71 | # Start the server by default, this can be overwritten at runtime
72 | EXPOSE 3000
73 | CMD ["./bin/rails", "server"]
74 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | # Turn false under Spring and add config.action_view.cache_template_loading = true.
12 | config.cache_classes = true
13 |
14 | # Eager loading loads your whole application. When running a single test locally,
15 | # this probably isn't necessary. It's a good idea to do in a continuous integration
16 | # system, or in some way before deploying your code.
17 | config.eager_load = ENV["CI"].present?
18 |
19 | # Configure public file server for tests with Cache-Control for performance.
20 | config.public_file_server.enabled = true
21 | config.public_file_server.headers = {
22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
23 | }
24 |
25 | # Show full error reports and disable caching.
26 | config.consider_all_requests_local = true
27 | config.action_controller.perform_caching = false
28 | config.cache_store = :null_store
29 |
30 | # Raise exceptions instead of rendering exception templates.
31 | config.action_dispatch.show_exceptions = false
32 |
33 | # Disable request forgery protection in test environment.
34 | config.action_controller.allow_forgery_protection = false
35 |
36 | # Store uploaded files on the local file system in a temporary directory.
37 | config.active_storage.service = :test
38 |
39 | config.action_mailer.perform_caching = false
40 |
41 | # Tell Action Mailer not to deliver emails to the real world.
42 | # The :test delivery method accumulates sent emails in the
43 | # ActionMailer::Base.deliveries array.
44 | config.action_mailer.delivery_method = :test
45 |
46 | # Print deprecation notices to the stderr.
47 | config.active_support.deprecation = :stderr
48 |
49 | # Raise exceptions for disallowed deprecations.
50 | config.active_support.disallowed_deprecation = :raise
51 |
52 | # Tell Active Support which deprecation messages to disallow.
53 | config.active_support.disallowed_deprecation_warnings = []
54 |
55 | # Raises error for missing translations.
56 | # config.i18n.raise_on_missing_translations = true
57 |
58 | # Annotate rendered view with file names.
59 | # config.action_view.annotate_rendered_view_with_filenames = true
60 | end
61 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.cache_classes = false
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable server timing
18 | config.server_timing = true
19 |
20 | # Enable/disable caching. By default caching is disabled.
21 | # Run rails dev:cache to toggle caching.
22 | if Rails.root.join("tmp/caching-dev.txt").exist?
23 | config.action_controller.perform_caching = true
24 | config.action_controller.enable_fragment_cache_logging = true
25 |
26 | config.cache_store = :memory_store
27 | config.public_file_server.headers = {
28 | "Cache-Control" => "public, max-age=#{2.days.to_i}"
29 | }
30 | else
31 | config.action_controller.perform_caching = false
32 |
33 | config.cache_store = :null_store
34 | end
35 |
36 | # Store uploaded files on the local file system (see config/storage.yml for options).
37 | config.active_storage.service = :local
38 |
39 | # Don't care if the mailer can't send.
40 | config.action_mailer.raise_delivery_errors = false
41 |
42 | config.action_mailer.perform_caching = false
43 |
44 | # Print deprecation notices to the Rails logger.
45 | config.active_support.deprecation = :log
46 |
47 | # Raise exceptions for disallowed deprecations.
48 | config.active_support.disallowed_deprecation = :raise
49 |
50 | # Tell Active Support which deprecation messages to disallow.
51 | config.active_support.disallowed_deprecation_warnings = []
52 |
53 | # Raise an error on page load if there are pending migrations.
54 | config.active_record.migration_error = :page_load
55 |
56 | # Highlight code that triggered database queries in logs.
57 | config.active_record.verbose_query_logs = true
58 |
59 | # Suppress logger output for asset requests.
60 | config.assets.quiet = true
61 |
62 | # Raises error for missing translations.
63 | # config.i18n.raise_on_missing_translations = true
64 |
65 | # Annotate rendered view with file names.
66 | # config.action_view.annotate_rendered_view_with_filenames = true
67 |
68 | # Uncomment if you wish to allow Action Cable access from any origin.
69 | # config.action_cable.disable_request_forgery_protection = true
70 | end
71 |
--------------------------------------------------------------------------------
/app/views/superview/turbo.rb:
--------------------------------------------------------------------------------
1 | module Superview
2 | module Turbo
3 | # Renders the metatags for setting up Turbo Drive.
4 | class MetaTags < ApplicationComponent
5 | attr_accessor \
6 | :method,
7 | :scroll,
8 | :exempts_page_from_cache,
9 | :exempts_page_from_preview,
10 | :page_requires_reload
11 |
12 | METHOD = :replace
13 | SCROLL = :reset
14 |
15 | def initialize(method: METHOD, scroll: SCROLL, exempts_page_from_preview: nil, exempts_page_from_cache: nil, page_requires_reload: nil)
16 | refreshes_with method: method, scroll: scroll
17 | @exempts_page_from_cache = exempts_page_from_cache
18 | @exempts_page_from_preview = exempts_page_from_preview
19 | @page_requires_reload = page_requires_reload
20 | end
21 |
22 | def template
23 | meta(name: "turbo-refresh-method", content: @method)
24 | meta(name: "turbo-refresh-scroll", content: @scroll)
25 | meta(name: "turbo-cache-control", content: "no-cache") if @exempts_page_from_cache
26 | meta(name: "turbo-cache-control", content: "no-preview") if @exempts_page_from_preview
27 | meta(name: "turbo-visit-control", content: "reload") if @page_requires_reload
28 | end
29 |
30 | def refreshes_with(method: METHOD, scroll: SCROLL)
31 | self.method = method
32 | self.scroll = scroll
33 | end
34 |
35 | def method=(value)
36 | raise ArgumentError, "Invalid refresh option '#{value}'" unless value.in?(%i[ replace morph ])
37 | @method = value
38 | end
39 |
40 | def scroll=(value)
41 | raise ArgumentError, "Invalid scroll option '#{value}'" unless value.in?(%i[ reset preserve ])
42 | @scroll = value
43 | end
44 | end
45 |
46 | module Helpers
47 | extend ActiveSupport::Concern
48 |
49 | included do
50 | register_element :turbo_cable_stream_source
51 | end
52 |
53 | class_methods do
54 | def turbo(*args, **kwargs, &block)
55 | @turbo_meta_tags = MetaTags.new(*args, **kwargs)
56 | define_method(:turbo, &block) if block
57 | end
58 |
59 | def turbo_meta_tags
60 | @turbo_meta_tags ||= MetaTags.new
61 | end
62 | end
63 |
64 | def turbo_stream_from(*streamables, **attributes)
65 | attributes[:channel] = attributes[:channel]&.to_s || "Turbo::StreamsChannel"
66 | attributes[:"signed-stream-name"] = ::Turbo::StreamsChannel.signed_stream_name(streamables)
67 | turbo_cable_stream_source **attributes, class: "hidden", style: "display: none;"
68 | end
69 |
70 | def stream_from(*streamables)
71 | streamables.each do |streamable|
72 | case streamable
73 | in association: ActiveRecord::Relation
74 | association.each { turbo_stream_from streamable }
75 | else
76 | turbo_stream_from streamable
77 | end
78 | end
79 | end
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | ruby "3.2.1"
5 |
6 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
7 | gem "rails", "~> 7.1"
8 |
9 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
10 | gem "sprockets-rails"
11 |
12 | # Use sqlite3 as the database for Active Record
13 | gem "sqlite3", "~> 1.4"
14 |
15 | # Use the Puma web server [https://github.com/puma/puma]
16 | gem "puma", "~> 6.0"
17 |
18 | # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
19 | gem "importmap-rails"
20 |
21 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
22 | gem "turbo-rails", "~> 2.0.0.pre.beta"
23 |
24 | # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
25 | gem "stimulus-rails"
26 |
27 | # Build JSON APIs with ease [https://github.com/rails/jbuilder]
28 | gem "jbuilder"
29 |
30 | # Use Redis adapter to run Action Cable in production
31 | gem "redis", "~> 4.0"
32 |
33 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
34 | # gem "kredis"
35 |
36 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
37 | # gem "bcrypt", "~> 3.1.7"
38 |
39 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
40 | gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
41 |
42 | # Reduces boot times through caching; required in config/boot.rb
43 | gem "bootsnap", require: false
44 |
45 | # Use Sass to process CSS
46 | # gem "sassc-rails"
47 |
48 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images]
49 | # gem "image_processing", "~> 1.2"
50 |
51 | group :development, :test do
52 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
53 | gem "debug", platforms: %i[ mri mingw x64_mingw ]
54 | end
55 |
56 | group :development do
57 | # Use console on exceptions pages [https://github.com/rails/web-console]
58 | gem "web-console"
59 |
60 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
61 | # gem "rack-mini-profiler"
62 |
63 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring]
64 | # gem "spring"
65 | end
66 |
67 | group :test do
68 | # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
69 | gem "capybara"
70 | gem "selenium-webdriver"
71 | gem "webdrivers"
72 | end
73 |
74 | gem "phlex-rails"
75 | gem "oxidizer", "~> 0.1.0", github: "rocketshipio/oxidizer"
76 |
77 | # gem "oxidizer", path: "~/Projects/rocketshipio/oxidizer"
78 | gem "dockerfile-rails", ">= 1.2", :group => :development
79 |
80 | gem "inputomatic", "~> 0.1.1"
81 |
82 | gem "rspec-rails", "~> 6.0"
83 |
84 | gem "litestack"#, github: "oldmoe/litestack"
85 | gem "superview", github: "rubymonolith/superview", branch: "main"
86 | gem "superform", github: "rubymonolith/superform", branch: "main"
87 |
88 | gem "rouge", "~> 4.1"
89 |
--------------------------------------------------------------------------------
/spec/rails_helper.rb:
--------------------------------------------------------------------------------
1 | # This file is copied to spec/ when you run 'rails generate rspec:install'
2 | require 'spec_helper'
3 | ENV['RAILS_ENV'] ||= 'test'
4 | require_relative '../config/environment'
5 | # Prevent database truncation if the environment is production
6 | abort("The Rails environment is running in production mode!") if Rails.env.production?
7 | require 'rspec/rails'
8 | # Add additional requires below this line. Rails is not loaded until this point!
9 |
10 | # Requires supporting ruby files with custom matchers and macros, etc, in
11 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
12 | # run as spec files by default. This means that files in spec/support that end
13 | # in _spec.rb will both be required and run as specs, causing the specs to be
14 | # run twice. It is recommended that you do not name files matching this glob to
15 | # end with _spec.rb. You can configure this pattern with the --pattern
16 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
17 | #
18 | # The following line is provided for convenience purposes. It has the downside
19 | # of increasing the boot-up time by auto-requiring all files in the support
20 | # directory. Alternatively, in the individual `*_spec.rb` files, manually
21 | # require only the support files necessary.
22 | #
23 | # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
24 |
25 | # Checks for pending migrations and applies them before tests are run.
26 | # If you are not using ActiveRecord, you can remove these lines.
27 | begin
28 | ActiveRecord::Migration.maintain_test_schema!
29 | rescue ActiveRecord::PendingMigrationError => e
30 | abort e.to_s.strip
31 | end
32 | RSpec.configure do |config|
33 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
34 | config.fixture_path = "#{::Rails.root}/spec/fixtures"
35 |
36 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
37 | # examples within a transaction, remove the following line or assign false
38 | # instead of true.
39 | config.use_transactional_fixtures = true
40 |
41 | # You can uncomment this line to turn off ActiveRecord support entirely.
42 | # config.use_active_record = false
43 |
44 | # RSpec Rails can automatically mix in different behaviours to your tests
45 | # based on their file location, for example enabling you to call `get` and
46 | # `post` in specs under `spec/controllers`.
47 | #
48 | # You can disable this behaviour by removing the line below, and instead
49 | # explicitly tag your specs with their type, e.g.:
50 | #
51 | # RSpec.describe UsersController, type: :controller do
52 | # # ...
53 | # end
54 | #
55 | # The different available types are documented in the features, such as in
56 | # https://rspec.info/features/6-0/rspec-rails
57 | config.infer_spec_type_from_file_location!
58 |
59 | # Filter lines from Rails gems in backtraces.
60 | config.filter_rails_from_backtrace!
61 | # arbitrary gems may also be filtered via:
62 | # config.filter_gems_from_backtrace("gem name")
63 | end
64 |
--------------------------------------------------------------------------------
/app/controllers/blogs/batch/posts_controller.rb:
--------------------------------------------------------------------------------
1 | class Blogs::Batch::PostsController < ApplicationController
2 | assign :posts, through: :blogs, from: :current_user
3 |
4 | # TODO: How can I compose `assign :posts, ... with `resources`?
5 | include Batchable
6 | def scope
7 | @blog.posts
8 | end
9 | # / TODO: How can I compose `assign :posts, ... with `resources`?
10 |
11 | class Index < ApplicationView
12 | attr_accessor :posts, :blog, :current_user, :selection
13 |
14 | def title = "#{@blog.title} Posts"
15 | def subtitle
16 | plain "Select posts from "
17 | show(@blog, :title)
18 | end
19 |
20 | turbo method: :morph, scroll: :preserve do
21 | stream_from @blog, @current_user, @posts
22 | end
23 |
24 | def template(&)
25 | render ApplicationForm.new(@selection, action: url_for) do |form|
26 | render TableComponent.new(items: @selection.items) do |table|
27 | table.column do |column|
28 | column.item do |item|
29 | render form.field(:selected).collection.field.input(type: :checkbox, checked: @selection.selected?(item.id), value: item.id)
30 | end
31 | end
32 | table.column("Title") { show(_1, :title) }
33 | table.column("Author") { show(_1.user, :name) }
34 | table.column("Status") { _1.status }
35 | table.column("Publish Date") { _1.publish_at&.to_formatted_s(:long) }
36 | end
37 |
38 | nav do
39 | ul do
40 | li do
41 | render form.field(:action).button(value: "delete")
42 | end
43 | li do
44 | render form.field(:action).button(value: "publish")
45 | end
46 | li do
47 | render form.field(:action).button(value: "unpublish")
48 | end
49 | li do
50 | if @selection.selected?
51 | render form.field(:action).button(value: "select_none")
52 | else
53 | render form.field(:action).button(value: "select_all")
54 | end
55 | end
56 | li do
57 | show(@blog) { "Back to Blog" }
58 | end
59 | end
60 | end
61 | end
62 | end
63 | end
64 |
65 | def delete
66 | @selection.selected_items.delete_all
67 | render phlex_action(:index), status: :created
68 | end
69 |
70 | def publish
71 | @selection.selected_items.update_all(publish_at: Time.current)
72 | render phlex_action(:index), status: :created
73 | end
74 |
75 | def unpublish
76 | @selection.selected_items.update_all(publish_at: nil)
77 | render phlex_action(:index), status: :created
78 | end
79 |
80 | def select_none
81 | @selection.select_none
82 | render phlex_action(:index), status: :created
83 | end
84 |
85 | def select_all
86 | @selection.select_all
87 | render phlex_action(:index), status: :created
88 | end
89 |
90 | private
91 |
92 | after_action :broadcasts_refreshes, if: :routable_batch_action?
93 | def broadcasts_refreshes
94 | @selection.selected_items.each(&:touch)
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'bundle' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "rubygems"
12 |
13 | m = Module.new do
14 | module_function
15 |
16 | def invoked_as_script?
17 | File.expand_path($0) == File.expand_path(__FILE__)
18 | end
19 |
20 | def env_var_version
21 | ENV["BUNDLER_VERSION"]
22 | end
23 |
24 | def cli_arg_version
25 | return unless invoked_as_script? # don't want to hijack other binstubs
26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27 | bundler_version = nil
28 | update_index = nil
29 | ARGV.each_with_index do |a, i|
30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31 | bundler_version = a
32 | end
33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34 | bundler_version = $1
35 | update_index = i
36 | end
37 | bundler_version
38 | end
39 |
40 | def gemfile
41 | gemfile = ENV["BUNDLE_GEMFILE"]
42 | return gemfile if gemfile && !gemfile.empty?
43 |
44 | File.expand_path("../Gemfile", __dir__)
45 | end
46 |
47 | def lockfile
48 | lockfile =
49 | case File.basename(gemfile)
50 | when "gems.rb" then gemfile.sub(/\.rb$/, ".locked")
51 | else "#{gemfile}.lock"
52 | end
53 | File.expand_path(lockfile)
54 | end
55 |
56 | def lockfile_version
57 | return unless File.file?(lockfile)
58 | lockfile_contents = File.read(lockfile)
59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60 | Regexp.last_match(1)
61 | end
62 |
63 | def bundler_requirement
64 | @bundler_requirement ||=
65 | env_var_version ||
66 | cli_arg_version ||
67 | bundler_requirement_for(lockfile_version)
68 | end
69 |
70 | def bundler_requirement_for(version)
71 | return "#{Gem::Requirement.default}.a" unless version
72 |
73 | bundler_gem_version = Gem::Version.new(version)
74 |
75 | bundler_gem_version.approximate_recommendation
76 | end
77 |
78 | def load_bundler!
79 | ENV["BUNDLE_GEMFILE"] ||= gemfile
80 |
81 | activate_bundler
82 | end
83 |
84 | def activate_bundler
85 | gem_error = activation_error_handling do
86 | gem "bundler", bundler_requirement
87 | end
88 | return if gem_error.nil?
89 | require_error = activation_error_handling do
90 | require "bundler/version"
91 | end
92 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
93 | 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}'`"
94 | exit 42
95 | end
96 |
97 | def activation_error_handling
98 | yield
99 | nil
100 | rescue StandardError, LoadError => e
101 | e
102 | end
103 | end
104 |
105 | m.load_bundler!
106 |
107 | if m.invoked_as_script?
108 | load Gem.bin_path("bundler", "bundle")
109 | end
110 |
--------------------------------------------------------------------------------
/app/controllers/concerns/assignable.rb:
--------------------------------------------------------------------------------
1 | module Assignable
2 | extend ActiveSupport::Concern
3 |
4 | included do
5 | class_attribute :model, :parent_model, :context_method_name
6 |
7 | before_action :assign_parent_collection, if: :has_parent_model?
8 | before_action :assign_parent_member, if: :has_parent_model?
9 | before_action :assign_collection
10 | before_action :assign_member
11 | end
12 |
13 | def assign_collection
14 | instance_variable_set "@#{model.model_name.plural}", model_scope
15 | end
16 |
17 | def assign_parent_collection
18 | instance_variable_set "@#{parent_model.model_name.plural}", parent_model_scope
19 | end
20 |
21 | def model_scope
22 | if has_parent_model?
23 | parent_model_instance.association(model.model_name.collection)
24 | elsif has_assignable_context?
25 | assignable_context.association(model.model_name.collection).scope
26 | else
27 | model.scope_for_association
28 | end
29 | end
30 |
31 | def parent_model_scope
32 | if has_assignable_context?
33 | assignable_context.association(parent_model.model_name.collection)
34 | else
35 | parent_model.scope_for_association
36 | end
37 | end
38 |
39 | def parent_model_instance
40 | parent_model_scope.find(params.fetch(parent_model_param_key))
41 | end
42 |
43 | def assign_parent_member
44 | instance_variable_set "@#{parent_model.model_name.singular}", parent_model_instance
45 | end
46 |
47 | def has_parent_model?
48 | parent_model.present?
49 | end
50 |
51 | def assign_member
52 | instance_variable_set "@#{model.model_name.singular}", model_instance
53 | end
54 |
55 | def model_instance
56 | if member?
57 | model_scope.find params.fetch(model_param_key)
58 | else
59 | model_scope.build.tap do |post|
60 | # # Blog is a reflection of User
61 | # # Get the name of the `user` association.
62 | # parent_from_association = parent_model_scope.reflection.inverse_of
63 |
64 | # if model.reflect_on_association(parent_from_association.name)
65 | # similar_association = model.association parent_from_association.name
66 | # # Now let's see if that association exists on the current_model ..
67 | # #
68 | # # This isn't setting the foreign key ... errrggggg.
69 | # raise 'hell'
70 |
71 | # # post.association(association_name).target = parent_model_scope.owner
72 | # end
73 | end
74 | end
75 | end
76 |
77 | def member?
78 | params.key? model_param_key
79 | end
80 |
81 | def model_param_key
82 | :id
83 | end
84 |
85 | def parent_model_param_key
86 | "#{parent_model.model_name.singular}_id".to_sym
87 | end
88 |
89 | def assignable_context
90 | self.send self.class.context_method_name
91 | end
92 |
93 | def has_assignable_context?
94 | if self.class.context_method_name
95 | self.respond_to? self.class.context_method_name
96 | end
97 | end
98 |
99 | class_methods do
100 | def assign(scope, through: nil, from: nil)
101 | self.model = Assignable.find_scope scope
102 | self.parent_model = Assignable.find_scope through
103 | self.context_method_name = from
104 | end
105 | end
106 |
107 | def self.find_scope(name)
108 | name.to_s.singularize.camelize.constantize if name
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # Code is not reloaded between requests.
7 | config.cache_classes = true
8 |
9 | # Eager load code on boot. This eager loads most of Rails and
10 | # your application in memory, allowing both threaded web servers
11 | # and those relying on copy on write to perform better.
12 | # Rake tasks automatically ignore this option for performance.
13 | config.eager_load = true
14 |
15 | # Full error reports are disabled and caching is turned on.
16 | config.consider_all_requests_local = false
17 | config.action_controller.perform_caching = true
18 |
19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
21 | # config.require_master_key = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
26 |
27 | # Compress CSS using a preprocessor.
28 | # config.assets.css_compressor = :sass
29 |
30 | # Do not fallback to assets pipeline if a precompiled asset is missed.
31 | config.assets.compile = false
32 |
33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
34 | # config.asset_host = "http://assets.example.com"
35 |
36 | # Specifies the header that your server uses for sending files.
37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
39 |
40 | # Store uploaded files on the local file system (see config/storage.yml for options).
41 | config.active_storage.service = :local
42 |
43 | # Mount Action Cable outside main process or domain.
44 | # config.action_cable.mount_path = nil
45 | # config.action_cable.url = "wss://example.com/cable"
46 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
47 |
48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
49 | # config.force_ssl = true
50 |
51 | # Include generic and useful information about system operation, but avoid logging too much
52 | # information to avoid inadvertent exposure of personally identifiable information (PII).
53 | config.log_level = :info
54 |
55 | # Prepend all log lines with the following tags.
56 | config.log_tags = [ :request_id ]
57 |
58 | # Use a different cache store in production.
59 | # config.cache_store = :litecache, { path: './path/to/your/cache/file' }
60 |
61 | # Use a real queuing backend for Active Job (and separate queues per environment).
62 | # config.active_job.queue_adapter = :litejob
63 | # config.active_job.queue_name_prefix = "resourcerer_production"
64 |
65 | config.action_mailer.perform_caching = false
66 |
67 | # Ignore bad email addresses and do not raise email delivery errors.
68 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
69 | # config.action_mailer.raise_delivery_errors = false
70 |
71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
72 | # the I18n.default_locale when a translation cannot be found).
73 | config.i18n.fallbacks = true
74 |
75 | # Don't log any deprecations.
76 | config.active_support.report_deprecations = false
77 |
78 | # Use default logging formatter so that PID and timestamp are not suppressed.
79 | config.log_formatter = ::Logger::Formatter.new
80 |
81 | # Use a different logger for distributed setups.
82 | # require "syslog/logger"
83 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
84 |
85 | if ENV["RAILS_LOG_TO_STDOUT"].present?
86 | logger = ActiveSupport::Logger.new(STDOUT)
87 | logger.formatter = config.log_formatter
88 | config.logger = ActiveSupport::TaggedLogging.new(logger)
89 | end
90 |
91 | # Do not dump schema after migrations.
92 | config.active_record.dump_schema_after_migration = false
93 | end
94 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all
2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3 | # The generated `.rspec` file contains `--require spec_helper` which will cause
4 | # this file to always be loaded, without a need to explicitly require it in any
5 | # files.
6 | #
7 | # Given that it is always loaded, you are encouraged to keep this file as
8 | # light-weight as possible. Requiring heavyweight dependencies from this file
9 | # will add to the boot time of your test suite on EVERY test run, even for an
10 | # individual file that may not need all of that loaded. Instead, consider making
11 | # a separate helper file that requires the additional dependencies and performs
12 | # the additional setup, and require it from the spec files that actually need
13 | # it.
14 | #
15 | # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
16 | RSpec.configure do |config|
17 | # rspec-expectations config goes here. You can use an alternate
18 | # assertion/expectation library such as wrong or the stdlib/minitest
19 | # assertions if you prefer.
20 | config.expect_with :rspec do |expectations|
21 | # This option will default to `true` in RSpec 4. It makes the `description`
22 | # and `failure_message` of custom matchers include text for helper methods
23 | # defined using `chain`, e.g.:
24 | # be_bigger_than(2).and_smaller_than(4).description
25 | # # => "be bigger than 2 and smaller than 4"
26 | # ...rather than:
27 | # # => "be bigger than 2"
28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
29 | end
30 |
31 | # rspec-mocks config goes here. You can use an alternate test double
32 | # library (such as bogus or mocha) by changing the `mock_with` option here.
33 | config.mock_with :rspec do |mocks|
34 | # Prevents you from mocking or stubbing a method that does not exist on
35 | # a real object. This is generally recommended, and will default to
36 | # `true` in RSpec 4.
37 | mocks.verify_partial_doubles = true
38 | end
39 |
40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will
41 | # have no way to turn it off -- the option exists only for backwards
42 | # compatibility in RSpec 3). It causes shared context metadata to be
43 | # inherited by the metadata hash of host groups and examples, rather than
44 | # triggering implicit auto-inclusion in groups with matching metadata.
45 | config.shared_context_metadata_behavior = :apply_to_host_groups
46 |
47 | # The settings below are suggested to provide a good initial experience
48 | # with RSpec, but feel free to customize to your heart's content.
49 | =begin
50 | # This allows you to limit a spec run to individual examples or groups
51 | # you care about by tagging them with `:focus` metadata. When nothing
52 | # is tagged with `:focus`, all examples get run. RSpec also provides
53 | # aliases for `it`, `describe`, and `context` that include `:focus`
54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
55 | config.filter_run_when_matching :focus
56 |
57 | # Allows RSpec to persist some state between runs in order to support
58 | # the `--only-failures` and `--next-failure` CLI options. We recommend
59 | # you configure your source control system to ignore this file.
60 | config.example_status_persistence_file_path = "spec/examples.txt"
61 |
62 | # Limits the available syntax to the non-monkey patched syntax that is
63 | # recommended. For more details, see:
64 | # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/
65 | config.disable_monkey_patching!
66 |
67 | # Many RSpec users commonly either run the entire suite or an individual
68 | # file, and it's useful to allow more verbose output when running an
69 | # individual spec file.
70 | if config.files_to_run.one?
71 | # Use the documentation formatter for detailed output,
72 | # unless a formatter has already been configured
73 | # (e.g. via a command-line flag).
74 | config.default_formatter = "doc"
75 | end
76 |
77 | # Print the 10 slowest examples and example groups at the
78 | # end of the spec run, to help surface which specs are running
79 | # particularly slow.
80 | config.profile_examples = 10
81 |
82 | # Run specs in random order to surface order dependencies. If you find an
83 | # order dependency and want to debug it, you can fix the order by providing
84 | # the seed, which is printed after each run.
85 | # --seed 1234
86 | config.order = :random
87 |
88 | # Seed global randomization in this process using the `--seed` CLI option.
89 | # Setting this allows you to use `--seed` to deterministically reproduce
90 | # test failures related to randomization by passing the same `--seed` value
91 | # as the one that triggered the failure.
92 | Kernel.srand config.seed
93 | =end
94 | end
95 |
--------------------------------------------------------------------------------
/spec/views/phlex/superform_spec.rb:
--------------------------------------------------------------------------------
1 | require "rails_helper"
2 |
3 | RSpec.describe Superform::Namespace do
4 | User = Data.define(:name, :email, :nicknames, :addresses)
5 | Address = Data.define(:id, :street, :city)
6 |
7 | let(:user) do
8 | User.new(
9 | name: "Brad",
10 | email: "brad@example.com",
11 | nicknames: ["Dude", "The Dude"],
12 | addresses: [
13 | Address.new(id: 100, street: "Main St", city: "Small Town"),
14 | Address.new(id: 200, street: "Big Blvd", city: "Metropolis")
15 | ]
16 | )
17 | end
18 |
19 | let(:form) do
20 | Superform::Namespace.new(:user, object: user)
21 | end
22 |
23 | describe "root" do
24 | subject { form }
25 | it "has key" do
26 | expect(form.key).to eql(:user)
27 | end
28 | context "DOM" do
29 | let(:dom) { Superform::DOM.new(form) }
30 | it "has name" do
31 | expect(dom.name).to eql("user")
32 | end
33 | it "has id" do
34 | expect(dom.id).to eql("user")
35 | end
36 | it "has title" do
37 | expect(dom.title).to eql("User")
38 | end
39 | end
40 | end
41 |
42 | describe "child" do
43 | let(:field) { form.field(:name) }
44 | subject { field }
45 | it "returns value from parent" do
46 | expect(subject.value).to eql "Brad"
47 | end
48 | context "DOM" do
49 | let(:dom) { Superform::DOM.new(field) }
50 |
51 | it "has name" do
52 | expect(dom.name).to eql("user[name]")
53 | end
54 | it "has id" do
55 | expect(dom.id).to eql("user_name")
56 | end
57 | it "has key" do
58 | expect(dom.key).to eql("name")
59 | end
60 | it "has title" do
61 | expect(dom.title).to eql("Name")
62 | end
63 | end
64 | end
65 |
66 | describe "#field" do
67 | let(:field) { form.namespace(:one).namespace(:two).namespace(:three).field(:four, value: 5) }
68 | subject{ field }
69 | it "returns value" do
70 | expect(subject.value).to eql 5
71 | end
72 | context "DOM" do
73 | let(:dom) { Superform::DOM.new(field) }
74 |
75 | it "has name" do
76 | expect(dom.name).to eql("user[one][two][three][four]")
77 | end
78 | it "has id" do
79 | expect(dom.id).to eql("user_one_two_three_four")
80 | end
81 | it "has key" do
82 | expect(dom.key).to eql("four")
83 | end
84 | it "has title" do
85 | expect(dom.title).to eql("Four")
86 | end
87 | end
88 | end
89 |
90 | describe "#values" do
91 | context "array of values" do
92 | let(:field) { form.collection(:nicknames).first }
93 | subject { field }
94 | it "returns value" do
95 | expect(subject.value).to eql "Dude"
96 | end
97 | it "returns key" do
98 | expect(subject.key).to eql 0
99 | end
100 | context "DOM" do
101 | let(:dom) { Superform::DOM.new(field) }
102 |
103 | it "has name" do
104 | expect(dom.name).to eql("user[nicknames][]")
105 | end
106 | it "has id" do
107 | expect(dom.id).to eql("user_nicknames_0")
108 | end
109 | it "has key" do
110 | expect(dom.key).to eql("0")
111 | end
112 | it "has title" do
113 | expect(dom.title).to eql("0")
114 | end
115 | end
116 | end
117 |
118 | context "array of objects" do
119 | let(:field) { form.collection(:addresses).first.field(:street) }
120 | subject { field }
121 | it "returns value" do
122 | expect(subject.value).to eql("Main St")
123 | end
124 | it "returns key" do
125 | expect(subject.key).to eql(:street)
126 | end
127 | context "DOM" do
128 | let(:dom) { Superform::DOM.new(field) }
129 |
130 | it "has name" do
131 | expect(dom.name).to eql("user[addresses][][street]")
132 | end
133 | it "has id" do
134 | expect(dom.id).to eql("user_addresses_0_street")
135 | end
136 | it "has key" do
137 | expect(dom.key).to eql("street")
138 | end
139 | it "has title" do
140 | expect(dom.title).to eql("Street")
141 | end
142 | end
143 | end
144 | end
145 |
146 | describe "mapping" do
147 | subject { form.to_h }
148 |
149 | before do
150 | form.namespace(:name) do |name|
151 | name.field(:first, value: "Brad")
152 | name.field(:last)
153 | end
154 | form.field(:email)
155 | form.collection(:nicknames)
156 | form.collection(:addresses) do |address|
157 | address.field(:id, permit: false)
158 | address.field(:street)
159 | end
160 | form.collection(:modulo, object: 4.times) do |modulo|
161 | if (modulo.value % 2 == 0)
162 | modulo.field(:fizz, value: modulo.value)
163 | else
164 | modulo.field(:buzz) do |buzz|
165 | buzz.field(:saw, value: modulo.value)
166 | end
167 | end
168 | end
169 | end
170 |
171 | describe "#to_h" do
172 | it do
173 | is_expected.to eql(
174 | name: { first: "Brad", last: "d" },
175 | nicknames: ["Dude", "The Dude"],
176 | email: "brad@example.com",
177 | addresses: [
178 | { id: 100, street: "Main St" },
179 | { id: 200, street: "Big Blvd" }
180 | ],
181 | modulo: [
182 | { fizz: 0 },
183 | { buzz: { saw: 1 }},
184 | { fizz: 2 },
185 | { buzz: { saw: 3 }}
186 | ]
187 | )
188 | end
189 | end
190 |
191 | describe "#assign" do
192 | let(:params) do
193 | {
194 | email: "bard@example.com",
195 | malicious_garbage: "haha! You will never win my pretty!",
196 | nicknames: nil,
197 | addresses: [
198 | {
199 | id: 999,
200 | street: "Lame Street",
201 | extra_garbage: "Super garbage"
202 | },
203 | {
204 | id: 888,
205 | street: "Bigsby Lane",
206 | malicious_garbage: "I will steal your address!"
207 | },
208 | {
209 | id: 777,
210 | street: "Amazing Avenue",
211 | lots_of_trash: "I will steal your address!"
212 | }
213 | ],
214 | modulo: [
215 | { fizz: 200, malicious_garbage: "I will foil your plans!" },
216 | { malicious_garbage: "The world will be mine!" }
217 | ]
218 | }
219 | end
220 | before { form.assign params }
221 |
222 | it "does not include fields not in list" do
223 | expect(subject.keys).to_not include :malicious_garbage
224 | end
225 |
226 | it "includes fields in list" do
227 | expect(form.to_h).to eql(
228 | name: { first: "Brad", last: "d" },
229 | nicknames: nil,
230 | email: "bard@example.com",
231 | addresses: [
232 | { id: 100, street: "Lame Street" },
233 | { id: 200, street: "Bigsby Lane" },
234 | { id: 777, street: "Amazing Avenue" }
235 | ],
236 | modulo: [
237 | { fizz: 200 },
238 | { buzz: { saw: 1 }},
239 | { fizz: 2 },
240 | { buzz: { saw: 3 }}
241 | ]
242 | )
243 | end
244 | end
245 | end
246 | end
247 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | Demonstrates the use of a Phlex app that maps closely to the database tables of an application.
4 |
5 | * [Demo](https://oxidizer-demo.fly.dev) - This repo deployed to [Fly.io](https://fly.io/docs/rails/)
6 | * [Component-driven Development on Rails with Phlex](https://fly.io/ruby-dispatch/component-driven-development-on-rails-with-phlex/) - Article that covers some of the more notable points of this demo repo.
7 | * [Phlex](https://www.phlex.fun) - Rubygem that generates HTML from Ruby classes.
8 |
9 | ## Embedded Phlex views in controllers
10 |
11 | Phlex classes are used to render HTML views. Now Erb, partials, or templates are used. This demonstrates the feasiblity of component-driven application development with Rails.
12 |
13 | Here's an example of a controller with embedded Phlex classes:
14 |
15 | ```ruby
16 | class Users::BlogsController < ApplicationController
17 | resources :blogs, from: :current_user
18 |
19 | class New < ApplicationView
20 | attr_writer :blog
21 |
22 | def template
23 | h1 { "Create a new blog" }
24 | render BlogsController::Form.new(@blog)
25 | end
26 | end
27 |
28 | class Index < ApplicationView
29 | attr_writer :blogs, :current_user
30 |
31 | def template(&)
32 | h1 { "#{@current_user.name}'s Blogs" }
33 | section do
34 | ul {
35 | @blogs.each { |blog|
36 | li { show(blog, :title) }
37 | }
38 | }
39 | create(@current_user.blogs, role: "button")
40 | end
41 | end
42 | end
43 | end
44 | ```
45 |
46 | ## Shallow RESTful applications
47 |
48 | This project picks up where Rails left off with Shallow RESTful routes. [Boring Rails](https://boringrails.com/tips/rails-scoped-controllers-sharing-code) does a decent job covering it, but as you'll see there's a lot to be desired.
49 |
50 | But first, it's important to understand the use of modules in controllers to manage the context in which things are created.
51 |
52 | For example, when creating a post for a blog, the URL would be `/blogs/100/posts/new`, which maps to the controller at `Blogs::PostsController#new`, which eventually creates the object via `User.find(session[:user_id]).blogs.find(params[:blog_id]).build(params.require(:post).permit(:title, :post)).create!` in ActiveRecord.
53 |
54 | It's really annoying typing that out every single time, so let's see how we can do better.
55 |
56 | ### Route helpers
57 |
58 | Routes look like this:
59 |
60 | ```ruby
61 | Rails.application.routes.draw do
62 | resources :blogs do
63 | nest :posts
64 | end
65 | end
66 | ```
67 |
68 |
69 | #### The old way
70 |
71 | If you did it the old way, you'd end up littering your routes file with `scope module: ...` calls, which makes the situation less readable.
72 |
73 | ```ruby
74 | Rails.application.routes.draw do
75 | resources :blogs do
76 | scope module: :blogs do
77 | resources :posts
78 | end
79 | end
80 | end
81 | ```
82 |
83 | ### Link helpers
84 |
85 | Link helpers are actually RESTful. Want to show a blog and have the link text be the title of the blog?
86 |
87 | ```ruby
88 | show(@blog, :title)
89 | ```
90 | Need to edit that blog?
91 |
92 | ```ruby
93 | edit(@blog)
94 | ```
95 |
96 | The text of the link defaults to "Edit Blog", but you can make it whatever you want by passing in a block:
97 |
98 | ```ruby
99 | edit(@blog) { "Edit the #{@blog.title} Blog" }
100 | ```
101 |
102 | Same for deleting the blog.
103 |
104 | ```ruby
105 | delete(@blog)
106 | ```
107 |
108 | Where things get interesting is creating stuff. If you pass a relationship into the blog helper, it will be able to infer its parent. For example, this
109 |
110 | ```ruby
111 | create(@blog.posts)
112 | ```
113 |
114 | Will understand that it should link to the `Blog::PostsController#new` action because it can reflect on the relationship.
115 |
116 | Similarly if you pass in an unpersisted model.
117 |
118 | ```ruby
119 | create(@blog)
120 | ```
121 |
122 | It will figure it out.
123 |
124 | #### The old way
125 |
126 | Rails started off with reasonable URL helpers. If you wanted to delete a resource, you could do something like this:
127 |
128 | ```erb
129 | <%=link_to "Delete Blog", @blog, method: :delete %>
130 | ```
131 |
132 | But then Turbo came along and for some reason things got more complicated because "consistency", so we ended up with this:
133 |
134 | ```erb
135 | <%= link_to "Delete Blog", @blog, data: {"turbo-method": :delete } %>
136 | ```
137 |
138 | Gah! Compare that to the new way:
139 |
140 | ```ruby
141 | delete(@blog)
142 | ```
143 |
144 | Creation is where things get more interesting, you're probably use to this:
145 |
146 | ```ruby
147 | <%= link_to "Create Blog Post", new_blog_post_path(@blog) %>
148 | ```
149 |
150 | The new way requires much less typing:
151 |
152 | ```ruby
153 | create(@blog.posts) { "Create Blog Post" }
154 | ```
155 |
156 | ### Controller helpers
157 |
158 | So much time is spent in Rails controllers writing code that loads data from params passed into the controller into ActiveRecord models.
159 |
160 | Oxidizer reduces that down to one line:
161 |
162 | ```ruby
163 | class Blogs::PostsController < ApplicationController
164 | assign :posts, through: :blogs, from: :current_user
165 | end
166 | ```
167 |
168 | From your views you'd have access to `@posts`, `@post`, `@blog`, `@blogs`.
169 |
170 | But wait, there's more! If you change `assign` to `resources`, you get that plus `@resource`, `@resources`, `@parent_resource`, and `@parent_resources` assigned so you can implement components against those variables that resemble scaffolding.
171 |
172 | ```ruby
173 | class Blogs::PostsController < ApplicationController
174 | resources :posts, through: :blogs, from: :current_user
175 | end
176 | ```
177 |
178 | It also defines reasonable default behaviors for creating, updating, and destroying resources.
179 |
180 | #### The old way
181 |
182 | To accomplish the same thing in your controller, you might have had to do something like this.
183 |
184 | ```ruby
185 | module Blogs
186 | class PostsController < ApplicationController
187 | before_action :set_blog
188 | before_action :set_post, only: %i[ show edit update destroy ]
189 |
190 | def index
191 | @posts = @blog.posts.all
192 | end
193 |
194 | # GET /posts/1 or /posts/1.json
195 | def show
196 | end
197 |
198 | # GET /posts/new
199 | def new
200 | @post = @blog.posts.build
201 | end
202 |
203 | # GET /posts/1/edit
204 | def edit
205 | end
206 |
207 | # POST /posts or /posts.json
208 | def create
209 | @post = Post.new(post_params)
210 |
211 | respond_to do |format|
212 | if @post.save
213 | format.html { redirect_to post_url(@post), notice: "Post was successfully created." }
214 | format.json { render :show, status: :created, location: @post }
215 | else
216 | format.html { render :new, status: :unprocessable_entity }
217 | format.json { render json: @post.errors, status: :unprocessable_entity }
218 | end
219 | end
220 | end
221 |
222 | # PATCH/PUT /posts/1 or /posts/1.json
223 | def update
224 | respond_to do |format|
225 | if @post.update(post_params)
226 | format.html { redirect_to post_url(@post), notice: "Post was successfully updated." }
227 | format.json { render :show, status: :ok, location: @post }
228 | else
229 | format.html { render :edit, status: :unprocessable_entity }
230 | format.json { render json: @post.errors, status: :unprocessable_entity }
231 | end
232 | end
233 | end
234 |
235 | # DELETE /posts/1 or /posts/1.json
236 | def destroy
237 | @post.destroy
238 |
239 | respond_to do |format|
240 | format.html { redirect_to posts_url, notice: "Post was successfully destroyed." }
241 | format.json { head :no_content }
242 | end
243 | end
244 |
245 | private
246 | # Use callbacks to share common setup or constraints between actions.
247 | def set_post
248 | @post = @blog.posts.find(params[:id])
249 | end
250 |
251 | def set_blog
252 | @blog = Blog.find(params[:blog_id])
253 | end
254 |
255 | # Only allow a list of trusted parameters through.
256 | def post_params
257 | params.fetch(:post, {}).permit(:title, :content)
258 | end
259 |
260 | # This is probably on the ApplicationController
261 | def current_user
262 | User.find session[:user_id]
263 | end
264 | end
265 | end
266 | ```
267 |
268 | It's possible to clean this up, which [Boring Rails](https://boringrails.com/tips/rails-scoped-controllers-sharing-code) writes about.
269 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GIT
2 | remote: https://github.com/rocketshipio/oxidizer.git
3 | revision: fe600852c4f47a1faf0d392476a71b0ee4fb0042
4 | specs:
5 | oxidizer (0.1.0)
6 | rails (>= 7.0.3)
7 |
8 | GIT
9 | remote: https://github.com/rubymonolith/superform.git
10 | revision: 0bf986c2a40bf3dda5b974266c6c0f9d008b28d5
11 | branch: main
12 | specs:
13 | superform (0.4.2)
14 | phlex-rails (~> 1.0)
15 | zeitwerk (~> 2.6)
16 |
17 | GIT
18 | remote: https://github.com/rubymonolith/superview.git
19 | revision: 2b15ea80b650a03de9975de2fe157bc35cfa4c27
20 | branch: main
21 | specs:
22 | superview (0.1.0)
23 | phlex-rails (~> 1.0)
24 |
25 | GEM
26 | remote: https://rubygems.org/
27 | specs:
28 | actioncable (7.1.2)
29 | actionpack (= 7.1.2)
30 | activesupport (= 7.1.2)
31 | nio4r (~> 2.0)
32 | websocket-driver (>= 0.6.1)
33 | zeitwerk (~> 2.6)
34 | actionmailbox (7.1.2)
35 | actionpack (= 7.1.2)
36 | activejob (= 7.1.2)
37 | activerecord (= 7.1.2)
38 | activestorage (= 7.1.2)
39 | activesupport (= 7.1.2)
40 | mail (>= 2.7.1)
41 | net-imap
42 | net-pop
43 | net-smtp
44 | actionmailer (7.1.2)
45 | actionpack (= 7.1.2)
46 | actionview (= 7.1.2)
47 | activejob (= 7.1.2)
48 | activesupport (= 7.1.2)
49 | mail (~> 2.5, >= 2.5.4)
50 | net-imap
51 | net-pop
52 | net-smtp
53 | rails-dom-testing (~> 2.2)
54 | actionpack (7.1.2)
55 | actionview (= 7.1.2)
56 | activesupport (= 7.1.2)
57 | nokogiri (>= 1.8.5)
58 | racc
59 | rack (>= 2.2.4)
60 | rack-session (>= 1.0.1)
61 | rack-test (>= 0.6.3)
62 | rails-dom-testing (~> 2.2)
63 | rails-html-sanitizer (~> 1.6)
64 | actiontext (7.1.2)
65 | actionpack (= 7.1.2)
66 | activerecord (= 7.1.2)
67 | activestorage (= 7.1.2)
68 | activesupport (= 7.1.2)
69 | globalid (>= 0.6.0)
70 | nokogiri (>= 1.8.5)
71 | actionview (7.1.2)
72 | activesupport (= 7.1.2)
73 | builder (~> 3.1)
74 | erubi (~> 1.11)
75 | rails-dom-testing (~> 2.2)
76 | rails-html-sanitizer (~> 1.6)
77 | activejob (7.1.2)
78 | activesupport (= 7.1.2)
79 | globalid (>= 0.3.6)
80 | activemodel (7.1.2)
81 | activesupport (= 7.1.2)
82 | activerecord (7.1.2)
83 | activemodel (= 7.1.2)
84 | activesupport (= 7.1.2)
85 | timeout (>= 0.4.0)
86 | activestorage (7.1.2)
87 | actionpack (= 7.1.2)
88 | activejob (= 7.1.2)
89 | activerecord (= 7.1.2)
90 | activesupport (= 7.1.2)
91 | marcel (~> 1.0)
92 | activesupport (7.1.2)
93 | base64
94 | bigdecimal
95 | concurrent-ruby (~> 1.0, >= 1.0.2)
96 | connection_pool (>= 2.2.5)
97 | drb
98 | i18n (>= 1.6, < 2)
99 | minitest (>= 5.1)
100 | mutex_m
101 | tzinfo (~> 2.0)
102 | addressable (2.8.6)
103 | public_suffix (>= 2.0.2, < 6.0)
104 | base64 (0.2.0)
105 | bigdecimal (3.1.4)
106 | bindex (0.8.1)
107 | bootsnap (1.17.0)
108 | msgpack (~> 1.2)
109 | builder (3.2.4)
110 | capybara (3.39.2)
111 | addressable
112 | matrix
113 | mini_mime (>= 0.1.3)
114 | nokogiri (~> 1.8)
115 | rack (>= 1.6.0)
116 | rack-test (>= 0.6.3)
117 | regexp_parser (>= 1.5, < 3.0)
118 | xpath (~> 3.2)
119 | cgi (0.4.1)
120 | chronic (0.10.2)
121 | concurrent-ruby (1.2.2)
122 | connection_pool (2.4.1)
123 | crass (1.0.6)
124 | date (3.3.4)
125 | debug (1.9.0)
126 | irb (~> 1.10)
127 | reline (>= 0.3.8)
128 | diff-lcs (1.5.0)
129 | dockerfile-rails (1.5.14)
130 | rails (>= 3.0.0)
131 | drb (2.2.0)
132 | ruby2_keywords
133 | erb (4.0.3)
134 | cgi (>= 0.3.3)
135 | erubi (1.12.0)
136 | globalid (1.2.1)
137 | activesupport (>= 6.1)
138 | hanami-router (2.0.2)
139 | mustermann (~> 3.0)
140 | mustermann-contrib (~> 3.0)
141 | rack (~> 2.0)
142 | hansi (0.2.1)
143 | i18n (1.14.1)
144 | concurrent-ruby (~> 1.0)
145 | importmap-rails (1.2.3)
146 | actionpack (>= 6.0.0)
147 | activesupport (>= 6.0.0)
148 | railties (>= 6.0.0)
149 | inputomatic (0.1.1)
150 | chronic (~> 0.10)
151 | io-console (0.6.0)
152 | irb (1.10.1)
153 | rdoc
154 | reline (>= 0.3.8)
155 | jbuilder (2.11.5)
156 | actionview (>= 5.0.0)
157 | activesupport (>= 5.0.0)
158 | litestack (0.4.2)
159 | erubi
160 | hanami-router
161 | oj
162 | rack
163 | sqlite3
164 | tilt
165 | loofah (2.22.0)
166 | crass (~> 1.0.2)
167 | nokogiri (>= 1.12.0)
168 | mail (2.8.1)
169 | mini_mime (>= 0.1.1)
170 | net-imap
171 | net-pop
172 | net-smtp
173 | marcel (1.0.2)
174 | matrix (0.4.2)
175 | mini_mime (1.1.5)
176 | minitest (5.20.0)
177 | msgpack (1.7.2)
178 | mustermann (3.0.0)
179 | ruby2_keywords (~> 0.0.1)
180 | mustermann-contrib (3.0.0)
181 | hansi (~> 0.2.0)
182 | mustermann (= 3.0.0)
183 | mutex_m (0.2.0)
184 | net-imap (0.4.7)
185 | date
186 | net-protocol
187 | net-pop (0.1.2)
188 | net-protocol
189 | net-protocol (0.2.2)
190 | timeout
191 | net-smtp (0.4.0)
192 | net-protocol
193 | nio4r (2.7.0)
194 | nokogiri (1.15.5-arm64-darwin)
195 | racc (~> 1.4)
196 | nokogiri (1.15.5-x86_64-linux)
197 | racc (~> 1.4)
198 | oj (3.16.2)
199 | bigdecimal (~> 3.1)
200 | phlex (1.9.0)
201 | concurrent-ruby (~> 1.2)
202 | erb (>= 4)
203 | zeitwerk (~> 2.6)
204 | phlex-rails (1.1.1)
205 | phlex (~> 1.9)
206 | railties (>= 6.1, < 8)
207 | zeitwerk (~> 2.6)
208 | psych (5.1.1.1)
209 | stringio
210 | public_suffix (5.0.4)
211 | puma (6.4.0)
212 | nio4r (~> 2.0)
213 | racc (1.7.3)
214 | rack (2.2.8)
215 | rack-session (1.0.2)
216 | rack (< 3)
217 | rack-test (2.1.0)
218 | rack (>= 1.3)
219 | rackup (1.0.0)
220 | rack (< 3)
221 | webrick
222 | rails (7.1.2)
223 | actioncable (= 7.1.2)
224 | actionmailbox (= 7.1.2)
225 | actionmailer (= 7.1.2)
226 | actionpack (= 7.1.2)
227 | actiontext (= 7.1.2)
228 | actionview (= 7.1.2)
229 | activejob (= 7.1.2)
230 | activemodel (= 7.1.2)
231 | activerecord (= 7.1.2)
232 | activestorage (= 7.1.2)
233 | activesupport (= 7.1.2)
234 | bundler (>= 1.15.0)
235 | railties (= 7.1.2)
236 | rails-dom-testing (2.2.0)
237 | activesupport (>= 5.0.0)
238 | minitest
239 | nokogiri (>= 1.6)
240 | rails-html-sanitizer (1.6.0)
241 | loofah (~> 2.21)
242 | nokogiri (~> 1.14)
243 | railties (7.1.2)
244 | actionpack (= 7.1.2)
245 | activesupport (= 7.1.2)
246 | irb
247 | rackup (>= 1.0.0)
248 | rake (>= 12.2)
249 | thor (~> 1.0, >= 1.2.2)
250 | zeitwerk (~> 2.6)
251 | rake (13.1.0)
252 | rdoc (6.6.1)
253 | psych (>= 4.0.0)
254 | redis (4.8.1)
255 | regexp_parser (2.8.3)
256 | reline (0.4.1)
257 | io-console (~> 0.5)
258 | rexml (3.2.6)
259 | rouge (4.2.0)
260 | rspec-core (3.12.2)
261 | rspec-support (~> 3.12.0)
262 | rspec-expectations (3.12.3)
263 | diff-lcs (>= 1.2.0, < 2.0)
264 | rspec-support (~> 3.12.0)
265 | rspec-mocks (3.12.6)
266 | diff-lcs (>= 1.2.0, < 2.0)
267 | rspec-support (~> 3.12.0)
268 | rspec-rails (6.1.0)
269 | actionpack (>= 6.1)
270 | activesupport (>= 6.1)
271 | railties (>= 6.1)
272 | rspec-core (~> 3.12)
273 | rspec-expectations (~> 3.12)
274 | rspec-mocks (~> 3.12)
275 | rspec-support (~> 3.12)
276 | rspec-support (3.12.1)
277 | ruby2_keywords (0.0.5)
278 | rubyzip (2.3.2)
279 | selenium-webdriver (4.10.0)
280 | rexml (~> 3.2, >= 3.2.5)
281 | rubyzip (>= 1.2.2, < 3.0)
282 | websocket (~> 1.0)
283 | sprockets (4.2.1)
284 | concurrent-ruby (~> 1.0)
285 | rack (>= 2.2.4, < 4)
286 | sprockets-rails (3.4.2)
287 | actionpack (>= 5.2)
288 | activesupport (>= 5.2)
289 | sprockets (>= 3.0.0)
290 | sqlite3 (1.6.9-arm64-darwin)
291 | sqlite3 (1.6.9-x86_64-linux)
292 | stimulus-rails (1.3.0)
293 | railties (>= 6.0.0)
294 | stringio (3.1.0)
295 | thor (1.3.0)
296 | tilt (2.3.0)
297 | timeout (0.4.1)
298 | turbo-rails (2.0.0.pre.beta.1)
299 | actionpack (>= 6.0.0)
300 | activejob (>= 6.0.0)
301 | railties (>= 6.0.0)
302 | tzinfo (2.0.6)
303 | concurrent-ruby (~> 1.0)
304 | web-console (4.2.1)
305 | actionview (>= 6.0.0)
306 | activemodel (>= 6.0.0)
307 | bindex (>= 0.4.0)
308 | railties (>= 6.0.0)
309 | webdrivers (5.3.1)
310 | nokogiri (~> 1.6)
311 | rubyzip (>= 1.3.0)
312 | selenium-webdriver (~> 4.0, < 4.11)
313 | webrick (1.8.1)
314 | websocket (1.2.10)
315 | websocket-driver (0.7.6)
316 | websocket-extensions (>= 0.1.0)
317 | websocket-extensions (0.1.5)
318 | xpath (3.2.0)
319 | nokogiri (~> 1.8)
320 | zeitwerk (2.6.12)
321 |
322 | PLATFORMS
323 | arm64-darwin-22
324 | arm64-darwin-23
325 | x86_64-linux
326 |
327 | DEPENDENCIES
328 | bootsnap
329 | capybara
330 | debug
331 | dockerfile-rails (>= 1.2)
332 | importmap-rails
333 | inputomatic (~> 0.1.1)
334 | jbuilder
335 | litestack
336 | oxidizer (~> 0.1.0)!
337 | phlex-rails
338 | puma (~> 6.0)
339 | rails (~> 7.1)
340 | redis (~> 4.0)
341 | rouge (~> 4.1)
342 | rspec-rails (~> 6.0)
343 | selenium-webdriver
344 | sprockets-rails
345 | sqlite3 (~> 1.4)
346 | stimulus-rails
347 | superform!
348 | superview!
349 | turbo-rails (~> 2.0.0.pre.beta)
350 | tzinfo-data
351 | web-console
352 | webdrivers
353 |
354 | RUBY VERSION
355 | ruby 3.2.1p31
356 |
357 | BUNDLED WITH
358 | 2.4.8
359 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/pico.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 | /*!
3 | * Pico CSS v1.5.9 (https://picocss.com)
4 | * Copyright 2019-2023 - Licensed under MIT
5 | */
6 | /**
7 | * Theme: default
8 | */
9 | :root {
10 | --font-family: system-ui, -apple-system, "Segoe UI", "Roboto", "Ubuntu",
11 | "Cantarell", "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
12 | "Segoe UI Symbol", "Noto Color Emoji";
13 | --line-height: 1.5;
14 | --font-weight: 400;
15 | --font-size: 16px;
16 | --border-radius: 0.25rem;
17 | --border-width: 1px;
18 | --outline-width: 3px;
19 | --spacing: 1rem;
20 | --typography-spacing-vertical: 1.5rem;
21 | --block-spacing-vertical: calc(var(--spacing) * 2);
22 | --block-spacing-horizontal: var(--spacing);
23 | --grid-spacing-vertical: 0;
24 | --grid-spacing-horizontal: var(--spacing);
25 | --form-element-spacing-vertical: 0.75rem;
26 | --form-element-spacing-horizontal: 1rem;
27 | --nav-element-spacing-vertical: 1rem;
28 | --nav-element-spacing-horizontal: 0.5rem;
29 | --nav-link-spacing-vertical: 0.5rem;
30 | --nav-link-spacing-horizontal: 0.5rem;
31 | --form-label-font-weight: var(--font-weight);
32 | --transition: 0.2s ease-in-out;
33 | --modal-overlay-backdrop-filter: blur(0.25rem);
34 | }
35 | @media (min-width: 576px) {
36 | :root {
37 | --font-size: 17px;
38 | }
39 | }
40 | @media (min-width: 768px) {
41 | :root {
42 | --font-size: 18px;
43 | }
44 | }
45 | @media (min-width: 992px) {
46 | :root {
47 | --font-size: 19px;
48 | }
49 | }
50 | @media (min-width: 1200px) {
51 | :root {
52 | --font-size: 20px;
53 | }
54 | }
55 |
56 | @media (min-width: 576px) {
57 | body > header,
58 | body > main,
59 | body > footer,
60 | section {
61 | --block-spacing-vertical: calc(var(--spacing) * 2.5);
62 | }
63 | }
64 | @media (min-width: 768px) {
65 | body > header,
66 | body > main,
67 | body > footer,
68 | section {
69 | --block-spacing-vertical: calc(var(--spacing) * 3);
70 | }
71 | }
72 | @media (min-width: 992px) {
73 | body > header,
74 | body > main,
75 | body > footer,
76 | section {
77 | --block-spacing-vertical: calc(var(--spacing) * 3.5);
78 | }
79 | }
80 | @media (min-width: 1200px) {
81 | body > header,
82 | body > main,
83 | body > footer,
84 | section {
85 | --block-spacing-vertical: calc(var(--spacing) * 4);
86 | }
87 | }
88 |
89 | @media (min-width: 576px) {
90 | article {
91 | --block-spacing-horizontal: calc(var(--spacing) * 1.25);
92 | }
93 | }
94 | @media (min-width: 768px) {
95 | article {
96 | --block-spacing-horizontal: calc(var(--spacing) * 1.5);
97 | }
98 | }
99 | @media (min-width: 992px) {
100 | article {
101 | --block-spacing-horizontal: calc(var(--spacing) * 1.75);
102 | }
103 | }
104 | @media (min-width: 1200px) {
105 | article {
106 | --block-spacing-horizontal: calc(var(--spacing) * 2);
107 | }
108 | }
109 |
110 | dialog > article {
111 | --block-spacing-vertical: calc(var(--spacing) * 2);
112 | --block-spacing-horizontal: var(--spacing);
113 | }
114 | @media (min-width: 576px) {
115 | dialog > article {
116 | --block-spacing-vertical: calc(var(--spacing) * 2.5);
117 | --block-spacing-horizontal: calc(var(--spacing) * 1.25);
118 | }
119 | }
120 | @media (min-width: 768px) {
121 | dialog > article {
122 | --block-spacing-vertical: calc(var(--spacing) * 3);
123 | --block-spacing-horizontal: calc(var(--spacing) * 1.5);
124 | }
125 | }
126 |
127 | a {
128 | --text-decoration: none;
129 | }
130 | a.secondary, a.contrast {
131 | --text-decoration: underline;
132 | }
133 |
134 | small {
135 | --font-size: 0.875em;
136 | }
137 |
138 | h1,
139 | h2,
140 | h3,
141 | h4,
142 | h5,
143 | h6 {
144 | --font-weight: 700;
145 | }
146 |
147 | h1 {
148 | --font-size: 2rem;
149 | --typography-spacing-vertical: 3rem;
150 | }
151 |
152 | h2 {
153 | --font-size: 1.75rem;
154 | --typography-spacing-vertical: 2.625rem;
155 | }
156 |
157 | h3 {
158 | --font-size: 1.5rem;
159 | --typography-spacing-vertical: 2.25rem;
160 | }
161 |
162 | h4 {
163 | --font-size: 1.25rem;
164 | --typography-spacing-vertical: 1.874rem;
165 | }
166 |
167 | h5 {
168 | --font-size: 1.125rem;
169 | --typography-spacing-vertical: 1.6875rem;
170 | }
171 |
172 | [type=checkbox],
173 | [type=radio] {
174 | --border-width: 2px;
175 | }
176 |
177 | [type=checkbox][role=switch] {
178 | --border-width: 3px;
179 | }
180 |
181 | thead th,
182 | thead td,
183 | tfoot th,
184 | tfoot td {
185 | --border-width: 3px;
186 | }
187 |
188 | :not(thead, tfoot) > * > td {
189 | --font-size: 0.875em;
190 | }
191 |
192 | pre,
193 | code,
194 | kbd,
195 | samp {
196 | --font-family: "Menlo", "Consolas", "Roboto Mono", "Ubuntu Monospace",
197 | "Noto Mono", "Oxygen Mono", "Liberation Mono", monospace,
198 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
199 | }
200 |
201 | kbd {
202 | --font-weight: bolder;
203 | }
204 |
205 | [data-theme=light],
206 | :root:not([data-theme=dark]) {
207 | --background-color: #fff;
208 | --color: hsl(205, 20%, 32%);
209 | --h1-color: hsl(205, 30%, 15%);
210 | --h2-color: #24333e;
211 | --h3-color: hsl(205, 25%, 23%);
212 | --h4-color: #374956;
213 | --h5-color: hsl(205, 20%, 32%);
214 | --h6-color: #4d606d;
215 | --muted-color: hsl(205, 10%, 50%);
216 | --muted-border-color: hsl(205, 20%, 94%);
217 | --primary: hsl(195, 85%, 41%);
218 | --primary-hover: hsl(195, 90%, 32%);
219 | --primary-focus: rgba(16, 149, 193, 0.125);
220 | --primary-inverse: #fff;
221 | --secondary: hsl(205, 15%, 41%);
222 | --secondary-hover: hsl(205, 20%, 32%);
223 | --secondary-focus: rgba(89, 107, 120, 0.125);
224 | --secondary-inverse: #fff;
225 | --contrast: hsl(205, 30%, 15%);
226 | --contrast-hover: #000;
227 | --contrast-focus: rgba(89, 107, 120, 0.125);
228 | --contrast-inverse: #fff;
229 | --mark-background-color: #fff2ca;
230 | --mark-color: #543a26;
231 | --ins-color: #388e3c;
232 | --del-color: #c62828;
233 | --blockquote-border-color: var(--muted-border-color);
234 | --blockquote-footer-color: var(--muted-color);
235 | --button-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
236 | --button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
237 | --form-element-background-color: transparent;
238 | --form-element-border-color: hsl(205, 14%, 68%);
239 | --form-element-color: var(--color);
240 | --form-element-placeholder-color: var(--muted-color);
241 | --form-element-active-background-color: transparent;
242 | --form-element-active-border-color: var(--primary);
243 | --form-element-focus-color: var(--primary-focus);
244 | --form-element-disabled-background-color: hsl(205, 18%, 86%);
245 | --form-element-disabled-border-color: hsl(205, 14%, 68%);
246 | --form-element-disabled-opacity: 0.5;
247 | --form-element-invalid-border-color: #c62828;
248 | --form-element-invalid-active-border-color: #d32f2f;
249 | --form-element-invalid-focus-color: rgba(211, 47, 47, 0.125);
250 | --form-element-valid-border-color: #388e3c;
251 | --form-element-valid-active-border-color: #43a047;
252 | --form-element-valid-focus-color: rgba(67, 160, 71, 0.125);
253 | --switch-background-color: hsl(205, 16%, 77%);
254 | --switch-color: var(--primary-inverse);
255 | --switch-checked-background-color: var(--primary);
256 | --range-border-color: hsl(205, 18%, 86%);
257 | --range-active-border-color: hsl(205, 16%, 77%);
258 | --range-thumb-border-color: var(--background-color);
259 | --range-thumb-color: var(--secondary);
260 | --range-thumb-hover-color: var(--secondary-hover);
261 | --range-thumb-active-color: var(--primary);
262 | --table-border-color: var(--muted-border-color);
263 | --table-row-stripped-background-color: #f6f8f9;
264 | --code-background-color: hsl(205, 20%, 94%);
265 | --code-color: var(--muted-color);
266 | --code-kbd-background-color: var(--contrast);
267 | --code-kbd-color: var(--contrast-inverse);
268 | --code-tag-color: hsl(330, 40%, 50%);
269 | --code-property-color: hsl(185, 40%, 40%);
270 | --code-value-color: hsl(40, 20%, 50%);
271 | --code-comment-color: hsl(205, 14%, 68%);
272 | --accordion-border-color: var(--muted-border-color);
273 | --accordion-close-summary-color: var(--color);
274 | --accordion-open-summary-color: var(--muted-color);
275 | --card-background-color: var(--background-color);
276 | --card-border-color: var(--muted-border-color);
277 | --card-box-shadow:
278 | 0.0145rem 0.029rem 0.174rem rgba(27, 40, 50, 0.01698),
279 | 0.0335rem 0.067rem 0.402rem rgba(27, 40, 50, 0.024),
280 | 0.0625rem 0.125rem 0.75rem rgba(27, 40, 50, 0.03),
281 | 0.1125rem 0.225rem 1.35rem rgba(27, 40, 50, 0.036),
282 | 0.2085rem 0.417rem 2.502rem rgba(27, 40, 50, 0.04302),
283 | 0.5rem 1rem 6rem rgba(27, 40, 50, 0.06),
284 | 0 0 0 0.0625rem rgba(27, 40, 50, 0.015);
285 | --card-sectionning-background-color: #fbfbfc;
286 | --dropdown-background-color: #fbfbfc;
287 | --dropdown-border-color: #e1e6eb;
288 | --dropdown-box-shadow: var(--card-box-shadow);
289 | --dropdown-color: var(--color);
290 | --dropdown-hover-background-color: hsl(205, 20%, 94%);
291 | --modal-overlay-background-color: rgba(213, 220, 226, 0.7);
292 | --progress-background-color: hsl(205, 18%, 86%);
293 | --progress-color: var(--primary);
294 | --loading-spinner-opacity: 0.5;
295 | --tooltip-background-color: var(--contrast);
296 | --tooltip-color: var(--contrast-inverse);
297 | --icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
298 | --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
299 | --icon-chevron-button: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
300 | --icon-chevron-button-inverse: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
301 | --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");
302 | --icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");
303 | --icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(198, 40, 40)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
304 | --icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");
305 | --icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");
306 | --icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");
307 | --icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(56, 142, 60)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
308 | color-scheme: light;
309 | }
310 |
311 | @media only screen and (prefers-color-scheme: dark) {
312 | :root:not([data-theme]) {
313 | --background-color: #11191f;
314 | --color: hsl(205, 16%, 77%);
315 | --h1-color: hsl(205, 20%, 94%);
316 | --h2-color: #e1e6eb;
317 | --h3-color: hsl(205, 18%, 86%);
318 | --h4-color: #c8d1d8;
319 | --h5-color: hsl(205, 16%, 77%);
320 | --h6-color: #afbbc4;
321 | --muted-color: hsl(205, 10%, 50%);
322 | --muted-border-color: #1f2d38;
323 | --primary: hsl(195, 85%, 41%);
324 | --primary-hover: hsl(195, 80%, 50%);
325 | --primary-focus: rgba(16, 149, 193, 0.25);
326 | --primary-inverse: #fff;
327 | --secondary: hsl(205, 15%, 41%);
328 | --secondary-hover: hsl(205, 10%, 50%);
329 | --secondary-focus: rgba(115, 130, 140, 0.25);
330 | --secondary-inverse: #fff;
331 | --contrast: hsl(205, 20%, 94%);
332 | --contrast-hover: #fff;
333 | --contrast-focus: rgba(115, 130, 140, 0.25);
334 | --contrast-inverse: #000;
335 | --mark-background-color: #d1c284;
336 | --mark-color: #11191f;
337 | --ins-color: #388e3c;
338 | --del-color: #c62828;
339 | --blockquote-border-color: var(--muted-border-color);
340 | --blockquote-footer-color: var(--muted-color);
341 | --button-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
342 | --button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
343 | --form-element-background-color: #11191f;
344 | --form-element-border-color: #374956;
345 | --form-element-color: var(--color);
346 | --form-element-placeholder-color: var(--muted-color);
347 | --form-element-active-background-color: var(--form-element-background-color);
348 | --form-element-active-border-color: var(--primary);
349 | --form-element-focus-color: var(--primary-focus);
350 | --form-element-disabled-background-color: hsl(205, 25%, 23%);
351 | --form-element-disabled-border-color: hsl(205, 20%, 32%);
352 | --form-element-disabled-opacity: 0.5;
353 | --form-element-invalid-border-color: #b71c1c;
354 | --form-element-invalid-active-border-color: #c62828;
355 | --form-element-invalid-focus-color: rgba(198, 40, 40, 0.25);
356 | --form-element-valid-border-color: #2e7d32;
357 | --form-element-valid-active-border-color: #388e3c;
358 | --form-element-valid-focus-color: rgba(56, 142, 60, 0.25);
359 | --switch-background-color: #374956;
360 | --switch-color: var(--primary-inverse);
361 | --switch-checked-background-color: var(--primary);
362 | --range-border-color: #24333e;
363 | --range-active-border-color: hsl(205, 25%, 23%);
364 | --range-thumb-border-color: var(--background-color);
365 | --range-thumb-color: var(--secondary);
366 | --range-thumb-hover-color: var(--secondary-hover);
367 | --range-thumb-active-color: var(--primary);
368 | --table-border-color: var(--muted-border-color);
369 | --table-row-stripped-background-color: rgba(115, 130, 140, 0.05);
370 | --code-background-color: #18232c;
371 | --code-color: var(--muted-color);
372 | --code-kbd-background-color: var(--contrast);
373 | --code-kbd-color: var(--contrast-inverse);
374 | --code-tag-color: hsl(330, 30%, 50%);
375 | --code-property-color: hsl(185, 30%, 50%);
376 | --code-value-color: hsl(40, 10%, 50%);
377 | --code-comment-color: #4d606d;
378 | --accordion-border-color: var(--muted-border-color);
379 | --accordion-active-summary-color: var(--primary);
380 | --accordion-close-summary-color: var(--color);
381 | --accordion-open-summary-color: var(--muted-color);
382 | --card-background-color: #141e26;
383 | --card-border-color: var(--card-background-color);
384 | --card-box-shadow:
385 | 0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698),
386 | 0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024),
387 | 0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03),
388 | 0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036),
389 | 0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302),
390 | 0.5rem 1rem 6rem rgba(0, 0, 0, 0.06),
391 | 0 0 0 0.0625rem rgba(0, 0, 0, 0.015);
392 | --card-sectionning-background-color: #18232c;
393 | --dropdown-background-color: hsl(205, 30%, 15%);
394 | --dropdown-border-color: #24333e;
395 | --dropdown-box-shadow: var(--card-box-shadow);
396 | --dropdown-color: var(--color);
397 | --dropdown-hover-background-color: rgba(36, 51, 62, 0.75);
398 | --modal-overlay-background-color: rgba(36, 51, 62, 0.8);
399 | --progress-background-color: #24333e;
400 | --progress-color: var(--primary);
401 | --loading-spinner-opacity: 0.5;
402 | --tooltip-background-color: var(--contrast);
403 | --tooltip-color: var(--contrast-inverse);
404 | --icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
405 | --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
406 | --icon-chevron-button: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
407 | --icon-chevron-button-inverse: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(0, 0, 0)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
408 | --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");
409 | --icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");
410 | --icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(183, 28, 28)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
411 | --icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");
412 | --icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");
413 | --icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");
414 | --icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(46, 125, 50)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
415 | color-scheme: dark;
416 | }
417 | }
418 | [data-theme=dark] {
419 | --background-color: #11191f;
420 | --color: hsl(205, 16%, 77%);
421 | --h1-color: hsl(205, 20%, 94%);
422 | --h2-color: #e1e6eb;
423 | --h3-color: hsl(205, 18%, 86%);
424 | --h4-color: #c8d1d8;
425 | --h5-color: hsl(205, 16%, 77%);
426 | --h6-color: #afbbc4;
427 | --muted-color: hsl(205, 10%, 50%);
428 | --muted-border-color: #1f2d38;
429 | --primary: hsl(195, 85%, 41%);
430 | --primary-hover: hsl(195, 80%, 50%);
431 | --primary-focus: rgba(16, 149, 193, 0.25);
432 | --primary-inverse: #fff;
433 | --secondary: hsl(205, 15%, 41%);
434 | --secondary-hover: hsl(205, 10%, 50%);
435 | --secondary-focus: rgba(115, 130, 140, 0.25);
436 | --secondary-inverse: #fff;
437 | --contrast: hsl(205, 20%, 94%);
438 | --contrast-hover: #fff;
439 | --contrast-focus: rgba(115, 130, 140, 0.25);
440 | --contrast-inverse: #000;
441 | --mark-background-color: #d1c284;
442 | --mark-color: #11191f;
443 | --ins-color: #388e3c;
444 | --del-color: #c62828;
445 | --blockquote-border-color: var(--muted-border-color);
446 | --blockquote-footer-color: var(--muted-color);
447 | --button-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
448 | --button-hover-box-shadow: 0 0 0 rgba(0, 0, 0, 0);
449 | --form-element-background-color: #11191f;
450 | --form-element-border-color: #374956;
451 | --form-element-color: var(--color);
452 | --form-element-placeholder-color: var(--muted-color);
453 | --form-element-active-background-color: var(--form-element-background-color);
454 | --form-element-active-border-color: var(--primary);
455 | --form-element-focus-color: var(--primary-focus);
456 | --form-element-disabled-background-color: hsl(205, 25%, 23%);
457 | --form-element-disabled-border-color: hsl(205, 20%, 32%);
458 | --form-element-disabled-opacity: 0.5;
459 | --form-element-invalid-border-color: #b71c1c;
460 | --form-element-invalid-active-border-color: #c62828;
461 | --form-element-invalid-focus-color: rgba(198, 40, 40, 0.25);
462 | --form-element-valid-border-color: #2e7d32;
463 | --form-element-valid-active-border-color: #388e3c;
464 | --form-element-valid-focus-color: rgba(56, 142, 60, 0.25);
465 | --switch-background-color: #374956;
466 | --switch-color: var(--primary-inverse);
467 | --switch-checked-background-color: var(--primary);
468 | --range-border-color: #24333e;
469 | --range-active-border-color: hsl(205, 25%, 23%);
470 | --range-thumb-border-color: var(--background-color);
471 | --range-thumb-color: var(--secondary);
472 | --range-thumb-hover-color: var(--secondary-hover);
473 | --range-thumb-active-color: var(--primary);
474 | --table-border-color: var(--muted-border-color);
475 | --table-row-stripped-background-color: rgba(115, 130, 140, 0.05);
476 | --code-background-color: #18232c;
477 | --code-color: var(--muted-color);
478 | --code-kbd-background-color: var(--contrast);
479 | --code-kbd-color: var(--contrast-inverse);
480 | --code-tag-color: hsl(330, 30%, 50%);
481 | --code-property-color: hsl(185, 30%, 50%);
482 | --code-value-color: hsl(40, 10%, 50%);
483 | --code-comment-color: #4d606d;
484 | --accordion-border-color: var(--muted-border-color);
485 | --accordion-active-summary-color: var(--primary);
486 | --accordion-close-summary-color: var(--color);
487 | --accordion-open-summary-color: var(--muted-color);
488 | --card-background-color: #141e26;
489 | --card-border-color: var(--card-background-color);
490 | --card-box-shadow:
491 | 0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698),
492 | 0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024),
493 | 0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03),
494 | 0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036),
495 | 0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302),
496 | 0.5rem 1rem 6rem rgba(0, 0, 0, 0.06),
497 | 0 0 0 0.0625rem rgba(0, 0, 0, 0.015);
498 | --card-sectionning-background-color: #18232c;
499 | --dropdown-background-color: hsl(205, 30%, 15%);
500 | --dropdown-border-color: #24333e;
501 | --dropdown-box-shadow: var(--card-box-shadow);
502 | --dropdown-color: var(--color);
503 | --dropdown-hover-background-color: rgba(36, 51, 62, 0.75);
504 | --modal-overlay-background-color: rgba(36, 51, 62, 0.8);
505 | --progress-background-color: #24333e;
506 | --progress-color: var(--primary);
507 | --loading-spinner-opacity: 0.5;
508 | --tooltip-background-color: var(--contrast);
509 | --tooltip-color: var(--contrast-inverse);
510 | --icon-checkbox: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
511 | --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
512 | --icon-chevron-button: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
513 | --icon-chevron-button-inverse: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(0, 0, 0)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
514 | --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");
515 | --icon-date: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");
516 | --icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(183, 28, 28)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");
517 | --icon-minus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");
518 | --icon-search: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");
519 | --icon-time: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");
520 | --icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(46, 125, 50)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");
521 | color-scheme: dark;
522 | }
523 |
524 | progress,
525 | [type=checkbox],
526 | [type=radio],
527 | [type=range] {
528 | accent-color: var(--primary);
529 | }
530 |
531 | /**
532 | * Document
533 | * Content-box & Responsive typography
534 | */
535 | *,
536 | *::before,
537 | *::after {
538 | box-sizing: border-box;
539 | background-repeat: no-repeat;
540 | }
541 |
542 | ::before,
543 | ::after {
544 | text-decoration: inherit;
545 | vertical-align: inherit;
546 | }
547 |
548 | :where(:root) {
549 | -webkit-tap-highlight-color: transparent;
550 | -webkit-text-size-adjust: 100%;
551 | -moz-text-size-adjust: 100%;
552 | text-size-adjust: 100%;
553 | background-color: var(--background-color);
554 | color: var(--color);
555 | font-weight: var(--font-weight);
556 | font-size: var(--font-size);
557 | line-height: var(--line-height);
558 | font-family: var(--font-family);
559 | text-rendering: optimizeLegibility;
560 | overflow-wrap: break-word;
561 | cursor: default;
562 | -moz-tab-size: 4;
563 | -o-tab-size: 4;
564 | tab-size: 4;
565 | }
566 |
567 | /**
568 | * Sectioning
569 | * Container and responsive spacings for header, main, footer
570 | */
571 | main {
572 | display: block;
573 | }
574 |
575 | body {
576 | width: 100%;
577 | margin: 0;
578 | }
579 | body > header,
580 | body > main,
581 | body > footer {
582 | width: 100%;
583 | margin-right: auto;
584 | margin-left: auto;
585 | padding: var(--block-spacing-vertical) 0;
586 | }
587 |
588 | /**
589 | * Container
590 | */
591 | .container,
592 | .container-fluid {
593 | width: 100%;
594 | margin-right: auto;
595 | margin-left: auto;
596 | padding-right: var(--spacing);
597 | padding-left: var(--spacing);
598 | }
599 |
600 | @media (min-width: 576px) {
601 | .container {
602 | max-width: 510px;
603 | padding-right: 0;
604 | padding-left: 0;
605 | }
606 | }
607 | @media (min-width: 768px) {
608 | .container {
609 | max-width: 700px;
610 | }
611 | }
612 | @media (min-width: 992px) {
613 | .container {
614 | max-width: 920px;
615 | }
616 | }
617 | @media (min-width: 1200px) {
618 | .container {
619 | max-width: 1130px;
620 | }
621 | }
622 |
623 | /**
624 | * Section
625 | * Responsive spacings for section
626 | */
627 | section {
628 | margin-bottom: var(--block-spacing-vertical);
629 | }
630 |
631 | /**
632 | * Grid
633 | * Minimal grid system with auto-layout columns
634 | */
635 | .grid {
636 | grid-column-gap: var(--grid-spacing-horizontal);
637 | grid-row-gap: var(--grid-spacing-vertical);
638 | display: grid;
639 | grid-template-columns: 1fr;
640 | margin: 0;
641 | }
642 | @media (min-width: 992px) {
643 | .grid {
644 | grid-template-columns: repeat(auto-fit, minmax(0%, 1fr));
645 | }
646 | }
647 | .grid > * {
648 | min-width: 0;
649 | }
650 |
651 | /**
652 | * Horizontal scroller ()
653 | */
654 | figure {
655 | display: block;
656 | margin: 0;
657 | padding: 0;
658 | overflow-x: auto;
659 | }
660 | figure figcaption {
661 | padding: calc(var(--spacing) * 0.5) 0;
662 | color: var(--muted-color);
663 | }
664 |
665 | /**
666 | * Typography
667 | */
668 | b,
669 | strong {
670 | font-weight: bolder;
671 | }
672 |
673 | sub,
674 | sup {
675 | position: relative;
676 | font-size: 0.75em;
677 | line-height: 0;
678 | vertical-align: baseline;
679 | }
680 |
681 | sub {
682 | bottom: -0.25em;
683 | }
684 |
685 | sup {
686 | top: -0.5em;
687 | }
688 |
689 | address,
690 | blockquote,
691 | dl,
692 | figure,
693 | form,
694 | ol,
695 | p,
696 | pre,
697 | table,
698 | ul {
699 | margin-top: 0;
700 | margin-bottom: var(--typography-spacing-vertical);
701 | color: var(--color);
702 | font-style: normal;
703 | font-weight: var(--font-weight);
704 | font-size: var(--font-size);
705 | }
706 |
707 | a,
708 | [role=link] {
709 | --color: var(--primary);
710 | --background-color: transparent;
711 | outline: none;
712 | background-color: var(--background-color);
713 | color: var(--color);
714 | -webkit-text-decoration: var(--text-decoration);
715 | text-decoration: var(--text-decoration);
716 | transition: background-color var(--transition), color var(--transition), box-shadow var(--transition), -webkit-text-decoration var(--transition);
717 | transition: background-color var(--transition), color var(--transition), text-decoration var(--transition), box-shadow var(--transition);
718 | transition: background-color var(--transition), color var(--transition), text-decoration var(--transition), box-shadow var(--transition), -webkit-text-decoration var(--transition);
719 | }
720 | a:is([aria-current], :hover, :active, :focus),
721 | [role=link]:is([aria-current], :hover, :active, :focus) {
722 | --color: var(--primary-hover);
723 | --text-decoration: underline;
724 | }
725 | a:focus,
726 | [role=link]:focus {
727 | --background-color: var(--primary-focus);
728 | }
729 | a.secondary,
730 | [role=link].secondary {
731 | --color: var(--secondary);
732 | }
733 | a.secondary:is([aria-current], :hover, :active, :focus),
734 | [role=link].secondary:is([aria-current], :hover, :active, :focus) {
735 | --color: var(--secondary-hover);
736 | }
737 | a.secondary:focus,
738 | [role=link].secondary:focus {
739 | --background-color: var(--secondary-focus);
740 | }
741 | a.contrast,
742 | [role=link].contrast {
743 | --color: var(--contrast);
744 | }
745 | a.contrast:is([aria-current], :hover, :active, :focus),
746 | [role=link].contrast:is([aria-current], :hover, :active, :focus) {
747 | --color: var(--contrast-hover);
748 | }
749 | a.contrast:focus,
750 | [role=link].contrast:focus {
751 | --background-color: var(--contrast-focus);
752 | }
753 |
754 | h1,
755 | h2,
756 | h3,
757 | h4,
758 | h5,
759 | h6 {
760 | margin-top: 0;
761 | margin-bottom: var(--typography-spacing-vertical);
762 | color: var(--color);
763 | font-weight: var(--font-weight);
764 | font-size: var(--font-size);
765 | font-family: var(--font-family);
766 | }
767 |
768 | h1 {
769 | --color: var(--h1-color);
770 | }
771 |
772 | h2 {
773 | --color: var(--h2-color);
774 | }
775 |
776 | h3 {
777 | --color: var(--h3-color);
778 | }
779 |
780 | h4 {
781 | --color: var(--h4-color);
782 | }
783 |
784 | h5 {
785 | --color: var(--h5-color);
786 | }
787 |
788 | h6 {
789 | --color: var(--h6-color);
790 | }
791 |
792 | :where(address, blockquote, dl, figure, form, ol, p, pre, table, ul) ~ :is(h1, h2, h3, h4, h5, h6) {
793 | margin-top: var(--typography-spacing-vertical);
794 | }
795 |
796 | hgroup,
797 | .headings {
798 | margin-bottom: var(--typography-spacing-vertical);
799 | }
800 | hgroup > *,
801 | .headings > * {
802 | margin-bottom: 0;
803 | }
804 | hgroup > *:last-child,
805 | .headings > *:last-child {
806 | --color: var(--muted-color);
807 | --font-weight: unset;
808 | font-size: 1rem;
809 | font-family: unset;
810 | }
811 |
812 | p {
813 | margin-bottom: var(--typography-spacing-vertical);
814 | }
815 |
816 | small {
817 | font-size: var(--font-size);
818 | }
819 |
820 | :where(dl, ol, ul) {
821 | padding-right: 0;
822 | padding-left: var(--spacing);
823 | -webkit-padding-start: var(--spacing);
824 | padding-inline-start: var(--spacing);
825 | -webkit-padding-end: 0;
826 | padding-inline-end: 0;
827 | }
828 | :where(dl, ol, ul) li {
829 | margin-bottom: calc(var(--typography-spacing-vertical) * 0.25);
830 | }
831 |
832 | :where(dl, ol, ul) :is(dl, ol, ul) {
833 | margin: 0;
834 | margin-top: calc(var(--typography-spacing-vertical) * 0.25);
835 | }
836 |
837 | ul li {
838 | list-style: square;
839 | }
840 |
841 | mark {
842 | padding: 0.125rem 0.25rem;
843 | background-color: var(--mark-background-color);
844 | color: var(--mark-color);
845 | vertical-align: baseline;
846 | }
847 |
848 | blockquote {
849 | display: block;
850 | margin: var(--typography-spacing-vertical) 0;
851 | padding: var(--spacing);
852 | border-right: none;
853 | border-left: 0.25rem solid var(--blockquote-border-color);
854 | -webkit-border-start: 0.25rem solid var(--blockquote-border-color);
855 | border-inline-start: 0.25rem solid var(--blockquote-border-color);
856 | -webkit-border-end: none;
857 | border-inline-end: none;
858 | }
859 | blockquote footer {
860 | margin-top: calc(var(--typography-spacing-vertical) * 0.5);
861 | color: var(--blockquote-footer-color);
862 | }
863 |
864 | abbr[title] {
865 | border-bottom: 1px dotted;
866 | text-decoration: none;
867 | cursor: help;
868 | }
869 |
870 | ins {
871 | color: var(--ins-color);
872 | text-decoration: none;
873 | }
874 |
875 | del {
876 | color: var(--del-color);
877 | }
878 |
879 | ::-moz-selection {
880 | background-color: var(--primary-focus);
881 | }
882 |
883 | ::selection {
884 | background-color: var(--primary-focus);
885 | }
886 |
887 | /**
888 | * Embedded content
889 | */
890 | :where(audio, canvas, iframe, img, svg, video) {
891 | vertical-align: middle;
892 | }
893 |
894 | audio,
895 | video {
896 | display: inline-block;
897 | }
898 |
899 | audio:not([controls]) {
900 | display: none;
901 | height: 0;
902 | }
903 |
904 | :where(iframe) {
905 | border-style: none;
906 | }
907 |
908 | img {
909 | max-width: 100%;
910 | height: auto;
911 | border-style: none;
912 | }
913 |
914 | :where(svg:not([fill])) {
915 | fill: currentColor;
916 | }
917 |
918 | svg:not(:root) {
919 | overflow: hidden;
920 | }
921 |
922 | /**
923 | * Button
924 | */
925 | button {
926 | margin: 0;
927 | overflow: visible;
928 | font-family: inherit;
929 | text-transform: none;
930 | }
931 |
932 | button,
933 | [type=button],
934 | [type=reset],
935 | [type=submit] {
936 | -webkit-appearance: button;
937 | }
938 |
939 | button {
940 | display: block;
941 | width: 100%;
942 | margin-bottom: var(--spacing);
943 | }
944 |
945 | [role=button] {
946 | display: inline-block;
947 | text-decoration: none;
948 | }
949 |
950 | button,
951 | input[type=submit],
952 | input[type=button],
953 | input[type=reset],
954 | [role=button] {
955 | --background-color: var(--primary);
956 | --border-color: var(--primary);
957 | --color: var(--primary-inverse);
958 | --box-shadow: var(--button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));
959 | padding: var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal);
960 | border: var(--border-width) solid var(--border-color);
961 | border-radius: var(--border-radius);
962 | outline: none;
963 | background-color: var(--background-color);
964 | box-shadow: var(--box-shadow);
965 | color: var(--color);
966 | font-weight: var(--font-weight);
967 | font-size: 1rem;
968 | line-height: var(--line-height);
969 | text-align: center;
970 | cursor: pointer;
971 | transition: background-color var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition);
972 | }
973 | button:is([aria-current], :hover, :active, :focus),
974 | input[type=submit]:is([aria-current], :hover, :active, :focus),
975 | input[type=button]:is([aria-current], :hover, :active, :focus),
976 | input[type=reset]:is([aria-current], :hover, :active, :focus),
977 | [role=button]:is([aria-current], :hover, :active, :focus) {
978 | --background-color: var(--primary-hover);
979 | --border-color: var(--primary-hover);
980 | --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));
981 | --color: var(--primary-inverse);
982 | }
983 | button:focus,
984 | input[type=submit]:focus,
985 | input[type=button]:focus,
986 | input[type=reset]:focus,
987 | [role=button]:focus {
988 | --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),
989 | 0 0 0 var(--outline-width) var(--primary-focus);
990 | }
991 |
992 | :is(button, input[type=submit], input[type=button], [role=button]).secondary,
993 | input[type=reset] {
994 | --background-color: var(--secondary);
995 | --border-color: var(--secondary);
996 | --color: var(--secondary-inverse);
997 | cursor: pointer;
998 | }
999 | :is(button, input[type=submit], input[type=button], [role=button]).secondary:is([aria-current], :hover, :active, :focus),
1000 | input[type=reset]:is([aria-current], :hover, :active, :focus) {
1001 | --background-color: var(--secondary-hover);
1002 | --border-color: var(--secondary-hover);
1003 | --color: var(--secondary-inverse);
1004 | }
1005 | :is(button, input[type=submit], input[type=button], [role=button]).secondary:focus,
1006 | input[type=reset]:focus {
1007 | --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),
1008 | 0 0 0 var(--outline-width) var(--secondary-focus);
1009 | }
1010 |
1011 | :is(button, input[type=submit], input[type=button], [role=button]).contrast {
1012 | --background-color: var(--contrast);
1013 | --border-color: var(--contrast);
1014 | --color: var(--contrast-inverse);
1015 | }
1016 | :is(button, input[type=submit], input[type=button], [role=button]).contrast:is([aria-current], :hover, :active, :focus) {
1017 | --background-color: var(--contrast-hover);
1018 | --border-color: var(--contrast-hover);
1019 | --color: var(--contrast-inverse);
1020 | }
1021 | :is(button, input[type=submit], input[type=button], [role=button]).contrast:focus {
1022 | --box-shadow: var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),
1023 | 0 0 0 var(--outline-width) var(--contrast-focus);
1024 | }
1025 |
1026 | :is(button, input[type=submit], input[type=button], [role=button]).outline,
1027 | input[type=reset].outline {
1028 | --background-color: transparent;
1029 | --color: var(--primary);
1030 | }
1031 | :is(button, input[type=submit], input[type=button], [role=button]).outline:is([aria-current], :hover, :active, :focus),
1032 | input[type=reset].outline:is([aria-current], :hover, :active, :focus) {
1033 | --background-color: transparent;
1034 | --color: var(--primary-hover);
1035 | }
1036 |
1037 | :is(button, input[type=submit], input[type=button], [role=button]).outline.secondary,
1038 | input[type=reset].outline {
1039 | --color: var(--secondary);
1040 | }
1041 | :is(button, input[type=submit], input[type=button], [role=button]).outline.secondary:is([aria-current], :hover, :active, :focus),
1042 | input[type=reset].outline:is([aria-current], :hover, :active, :focus) {
1043 | --color: var(--secondary-hover);
1044 | }
1045 |
1046 | :is(button, input[type=submit], input[type=button], [role=button]).outline.contrast {
1047 | --color: var(--contrast);
1048 | }
1049 | :is(button, input[type=submit], input[type=button], [role=button]).outline.contrast:is([aria-current], :hover, :active, :focus) {
1050 | --color: var(--contrast-hover);
1051 | }
1052 |
1053 | :where(button, [type=submit], [type=button], [type=reset], [role=button])[disabled],
1054 | :where(fieldset[disabled]) :is(button, [type=submit], [type=button], [type=reset], [role=button]),
1055 | a[role=button]:not([href]) {
1056 | opacity: 0.5;
1057 | pointer-events: none;
1058 | }
1059 |
1060 | /**
1061 | * Form elements
1062 | */
1063 | input,
1064 | optgroup,
1065 | select,
1066 | textarea {
1067 | margin: 0;
1068 | font-size: 1rem;
1069 | line-height: var(--line-height);
1070 | font-family: inherit;
1071 | letter-spacing: inherit;
1072 | }
1073 |
1074 | input {
1075 | overflow: visible;
1076 | }
1077 |
1078 | select {
1079 | text-transform: none;
1080 | }
1081 |
1082 | legend {
1083 | max-width: 100%;
1084 | padding: 0;
1085 | color: inherit;
1086 | white-space: normal;
1087 | }
1088 |
1089 | textarea {
1090 | overflow: auto;
1091 | }
1092 |
1093 | [type=checkbox],
1094 | [type=radio] {
1095 | padding: 0;
1096 | }
1097 |
1098 | ::-webkit-inner-spin-button,
1099 | ::-webkit-outer-spin-button {
1100 | height: auto;
1101 | }
1102 |
1103 | [type=search] {
1104 | -webkit-appearance: textfield;
1105 | outline-offset: -2px;
1106 | }
1107 |
1108 | [type=search]::-webkit-search-decoration {
1109 | -webkit-appearance: none;
1110 | }
1111 |
1112 | ::-webkit-file-upload-button {
1113 | -webkit-appearance: button;
1114 | font: inherit;
1115 | }
1116 |
1117 | ::-moz-focus-inner {
1118 | padding: 0;
1119 | border-style: none;
1120 | }
1121 |
1122 | :-moz-focusring {
1123 | outline: none;
1124 | }
1125 |
1126 | :-moz-ui-invalid {
1127 | box-shadow: none;
1128 | }
1129 |
1130 | ::-ms-expand {
1131 | display: none;
1132 | }
1133 |
1134 | [type=file],
1135 | [type=range] {
1136 | padding: 0;
1137 | border-width: 0;
1138 | }
1139 |
1140 | input:not([type=checkbox], [type=radio], [type=range]) {
1141 | height: calc(1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + var(--border-width) * 2);
1142 | }
1143 |
1144 | fieldset {
1145 | margin: 0;
1146 | margin-bottom: var(--spacing);
1147 | padding: 0;
1148 | border: 0;
1149 | }
1150 |
1151 | label,
1152 | fieldset legend {
1153 | display: block;
1154 | margin-bottom: calc(var(--spacing) * 0.25);
1155 | font-weight: var(--form-label-font-weight, var(--font-weight));
1156 | }
1157 |
1158 | input:not([type=checkbox], [type=radio]),
1159 | select,
1160 | textarea {
1161 | width: 100%;
1162 | }
1163 |
1164 | input:not([type=checkbox], [type=radio], [type=range], [type=file]),
1165 | select,
1166 | textarea {
1167 | -webkit-appearance: none;
1168 | -moz-appearance: none;
1169 | appearance: none;
1170 | padding: var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal);
1171 | }
1172 |
1173 | input,
1174 | select,
1175 | textarea {
1176 | --background-color: var(--form-element-background-color);
1177 | --border-color: var(--form-element-border-color);
1178 | --color: var(--form-element-color);
1179 | --box-shadow: none;
1180 | border: var(--border-width) solid var(--border-color);
1181 | border-radius: var(--border-radius);
1182 | outline: none;
1183 | background-color: var(--background-color);
1184 | box-shadow: var(--box-shadow);
1185 | color: var(--color);
1186 | font-weight: var(--font-weight);
1187 | transition: background-color var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition);
1188 | }
1189 |
1190 | input:not([type=submit], [type=button], [type=reset], [type=checkbox], [type=radio], [readonly]):is(:active, :focus),
1191 | :where(select, textarea):is(:active, :focus) {
1192 | --background-color: var(--form-element-active-background-color);
1193 | }
1194 |
1195 | input:not([type=submit], [type=button], [type=reset], [role=switch], [readonly]):is(:active, :focus),
1196 | :where(select, textarea):is(:active, :focus) {
1197 | --border-color: var(--form-element-active-border-color);
1198 | }
1199 |
1200 | input:not([type=submit], [type=button], [type=reset], [type=range], [type=file], [readonly]):focus,
1201 | select:focus,
1202 | textarea:focus {
1203 | --box-shadow: 0 0 0 var(--outline-width) var(--form-element-focus-color);
1204 | }
1205 |
1206 | input:not([type=submit], [type=button], [type=reset])[disabled],
1207 | select[disabled],
1208 | textarea[disabled],
1209 | :where(fieldset[disabled]) :is(input:not([type=submit], [type=button], [type=reset]), select, textarea) {
1210 | --background-color: var(--form-element-disabled-background-color);
1211 | --border-color: var(--form-element-disabled-border-color);
1212 | opacity: var(--form-element-disabled-opacity);
1213 | pointer-events: none;
1214 | }
1215 |
1216 | :where(input, select, textarea):not([type=checkbox], [type=radio], [type=date], [type=datetime-local], [type=month], [type=time], [type=week])[aria-invalid] {
1217 | padding-right: calc(var(--form-element-spacing-horizontal) + 1.5rem) !important;
1218 | padding-left: var(--form-element-spacing-horizontal);
1219 | -webkit-padding-start: var(--form-element-spacing-horizontal) !important;
1220 | padding-inline-start: var(--form-element-spacing-horizontal) !important;
1221 | -webkit-padding-end: calc(var(--form-element-spacing-horizontal) + 1.5rem) !important;
1222 | padding-inline-end: calc(var(--form-element-spacing-horizontal) + 1.5rem) !important;
1223 | background-position: center right 0.75rem;
1224 | background-size: 1rem auto;
1225 | background-repeat: no-repeat;
1226 | }
1227 | :where(input, select, textarea):not([type=checkbox], [type=radio], [type=date], [type=datetime-local], [type=month], [type=time], [type=week])[aria-invalid=false] {
1228 | background-image: var(--icon-valid);
1229 | }
1230 | :where(input, select, textarea):not([type=checkbox], [type=radio], [type=date], [type=datetime-local], [type=month], [type=time], [type=week])[aria-invalid=true] {
1231 | background-image: var(--icon-invalid);
1232 | }
1233 | :where(input, select, textarea)[aria-invalid=false] {
1234 | --border-color: var(--form-element-valid-border-color);
1235 | }
1236 | :where(input, select, textarea)[aria-invalid=false]:is(:active, :focus) {
1237 | --border-color: var(--form-element-valid-active-border-color) !important;
1238 | --box-shadow: 0 0 0 var(--outline-width) var(--form-element-valid-focus-color) !important;
1239 | }
1240 | :where(input, select, textarea)[aria-invalid=true] {
1241 | --border-color: var(--form-element-invalid-border-color);
1242 | }
1243 | :where(input, select, textarea)[aria-invalid=true]:is(:active, :focus) {
1244 | --border-color: var(--form-element-invalid-active-border-color) !important;
1245 | --box-shadow: 0 0 0 var(--outline-width) var(--form-element-invalid-focus-color) !important;
1246 | }
1247 |
1248 | [dir=rtl] :where(input, select, textarea):not([type=checkbox], [type=radio]):is([aria-invalid], [aria-invalid=true], [aria-invalid=false]) {
1249 | background-position: center left 0.75rem;
1250 | }
1251 |
1252 | input::placeholder,
1253 | input::-webkit-input-placeholder,
1254 | textarea::placeholder,
1255 | textarea::-webkit-input-placeholder,
1256 | select:invalid {
1257 | color: var(--form-element-placeholder-color);
1258 | opacity: 1;
1259 | }
1260 |
1261 | input:not([type=checkbox], [type=radio]),
1262 | select,
1263 | textarea {
1264 | margin-bottom: var(--spacing);
1265 | }
1266 |
1267 | select::-ms-expand {
1268 | border: 0;
1269 | background-color: transparent;
1270 | }
1271 | select:not([multiple], [size]) {
1272 | padding-right: calc(var(--form-element-spacing-horizontal) + 1.5rem);
1273 | padding-left: var(--form-element-spacing-horizontal);
1274 | -webkit-padding-start: var(--form-element-spacing-horizontal);
1275 | padding-inline-start: var(--form-element-spacing-horizontal);
1276 | -webkit-padding-end: calc(var(--form-element-spacing-horizontal) + 1.5rem);
1277 | padding-inline-end: calc(var(--form-element-spacing-horizontal) + 1.5rem);
1278 | background-image: var(--icon-chevron);
1279 | background-position: center right 0.75rem;
1280 | background-size: 1rem auto;
1281 | background-repeat: no-repeat;
1282 | }
1283 |
1284 | [dir=rtl] select:not([multiple], [size]) {
1285 | background-position: center left 0.75rem;
1286 | }
1287 |
1288 | :where(input, select, textarea, .grid) + small {
1289 | display: block;
1290 | width: 100%;
1291 | margin-top: calc(var(--spacing) * -0.75);
1292 | margin-bottom: var(--spacing);
1293 | color: var(--muted-color);
1294 | }
1295 |
1296 | label > :where(input, select, textarea) {
1297 | margin-top: calc(var(--spacing) * 0.25);
1298 | }
1299 |
1300 | /**
1301 | * Form elements
1302 | * Checkboxes & Radios
1303 | */
1304 | [type=checkbox],
1305 | [type=radio] {
1306 | -webkit-appearance: none;
1307 | -moz-appearance: none;
1308 | appearance: none;
1309 | width: 1.25em;
1310 | height: 1.25em;
1311 | margin-top: -0.125em;
1312 | margin-right: 0.375em;
1313 | margin-left: 0;
1314 | -webkit-margin-start: 0;
1315 | margin-inline-start: 0;
1316 | -webkit-margin-end: 0.375em;
1317 | margin-inline-end: 0.375em;
1318 | border-width: var(--border-width);
1319 | font-size: inherit;
1320 | vertical-align: middle;
1321 | cursor: pointer;
1322 | }
1323 | [type=checkbox]::-ms-check,
1324 | [type=radio]::-ms-check {
1325 | display: none;
1326 | }
1327 | [type=checkbox]:checked, [type=checkbox]:checked:active, [type=checkbox]:checked:focus,
1328 | [type=radio]:checked,
1329 | [type=radio]:checked:active,
1330 | [type=radio]:checked:focus {
1331 | --background-color: var(--primary);
1332 | --border-color: var(--primary);
1333 | background-image: var(--icon-checkbox);
1334 | background-position: center;
1335 | background-size: 0.75em auto;
1336 | background-repeat: no-repeat;
1337 | }
1338 | [type=checkbox] ~ label,
1339 | [type=radio] ~ label {
1340 | display: inline-block;
1341 | margin-right: 0.375em;
1342 | margin-bottom: 0;
1343 | cursor: pointer;
1344 | }
1345 |
1346 | [type=checkbox]:indeterminate {
1347 | --background-color: var(--primary);
1348 | --border-color: var(--primary);
1349 | background-image: var(--icon-minus);
1350 | background-position: center;
1351 | background-size: 0.75em auto;
1352 | background-repeat: no-repeat;
1353 | }
1354 |
1355 | [type=radio] {
1356 | border-radius: 50%;
1357 | }
1358 | [type=radio]:checked, [type=radio]:checked:active, [type=radio]:checked:focus {
1359 | --background-color: var(--primary-inverse);
1360 | border-width: 0.35em;
1361 | background-image: none;
1362 | }
1363 |
1364 | [type=checkbox][role=switch] {
1365 | --background-color: var(--switch-background-color);
1366 | --border-color: var(--switch-background-color);
1367 | --color: var(--switch-color);
1368 | width: 2.25em;
1369 | height: 1.25em;
1370 | border: var(--border-width) solid var(--border-color);
1371 | border-radius: 1.25em;
1372 | background-color: var(--background-color);
1373 | line-height: 1.25em;
1374 | }
1375 | [type=checkbox][role=switch]:focus {
1376 | --background-color: var(--switch-background-color);
1377 | --border-color: var(--switch-background-color);
1378 | }
1379 | [type=checkbox][role=switch]:checked {
1380 | --background-color: var(--switch-checked-background-color);
1381 | --border-color: var(--switch-checked-background-color);
1382 | }
1383 | [type=checkbox][role=switch]:before {
1384 | display: block;
1385 | width: calc(1.25em - (var(--border-width) * 2));
1386 | height: 100%;
1387 | border-radius: 50%;
1388 | background-color: var(--color);
1389 | content: "";
1390 | transition: margin 0.1s ease-in-out;
1391 | }
1392 | [type=checkbox][role=switch]:checked {
1393 | background-image: none;
1394 | }
1395 | [type=checkbox][role=switch]:checked::before {
1396 | margin-left: calc(1.125em - var(--border-width));
1397 | -webkit-margin-start: calc(1.125em - var(--border-width));
1398 | margin-inline-start: calc(1.125em - var(--border-width));
1399 | }
1400 |
1401 | [type=checkbox][aria-invalid=false],
1402 | [type=checkbox]:checked[aria-invalid=false],
1403 | [type=radio][aria-invalid=false],
1404 | [type=radio]:checked[aria-invalid=false],
1405 | [type=checkbox][role=switch][aria-invalid=false],
1406 | [type=checkbox][role=switch]:checked[aria-invalid=false] {
1407 | --border-color: var(--form-element-valid-border-color);
1408 | }
1409 | [type=checkbox][aria-invalid=true],
1410 | [type=checkbox]:checked[aria-invalid=true],
1411 | [type=radio][aria-invalid=true],
1412 | [type=radio]:checked[aria-invalid=true],
1413 | [type=checkbox][role=switch][aria-invalid=true],
1414 | [type=checkbox][role=switch]:checked[aria-invalid=true] {
1415 | --border-color: var(--form-element-invalid-border-color);
1416 | }
1417 |
1418 | /**
1419 | * Form elements
1420 | * Alternatives input types (Not Checkboxes & Radios)
1421 | */
1422 | [type=color]::-webkit-color-swatch-wrapper {
1423 | padding: 0;
1424 | }
1425 | [type=color]::-moz-focus-inner {
1426 | padding: 0;
1427 | }
1428 | [type=color]::-webkit-color-swatch {
1429 | border: 0;
1430 | border-radius: calc(var(--border-radius) * 0.5);
1431 | }
1432 | [type=color]::-moz-color-swatch {
1433 | border: 0;
1434 | border-radius: calc(var(--border-radius) * 0.5);
1435 | }
1436 |
1437 | input:not([type=checkbox], [type=radio], [type=range], [type=file]):is([type=date], [type=datetime-local], [type=month], [type=time], [type=week]) {
1438 | --icon-position: 0.75rem;
1439 | --icon-width: 1rem;
1440 | padding-right: calc(var(--icon-width) + var(--icon-position));
1441 | background-image: var(--icon-date);
1442 | background-position: center right var(--icon-position);
1443 | background-size: var(--icon-width) auto;
1444 | background-repeat: no-repeat;
1445 | }
1446 | input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=time] {
1447 | background-image: var(--icon-time);
1448 | }
1449 |
1450 | [type=date]::-webkit-calendar-picker-indicator,
1451 | [type=datetime-local]::-webkit-calendar-picker-indicator,
1452 | [type=month]::-webkit-calendar-picker-indicator,
1453 | [type=time]::-webkit-calendar-picker-indicator,
1454 | [type=week]::-webkit-calendar-picker-indicator {
1455 | width: var(--icon-width);
1456 | margin-right: calc(var(--icon-width) * -1);
1457 | margin-left: var(--icon-position);
1458 | opacity: 0;
1459 | }
1460 |
1461 | [dir=rtl] :is([type=date], [type=datetime-local], [type=month], [type=time], [type=week]) {
1462 | text-align: right;
1463 | }
1464 |
1465 | [type=file] {
1466 | --color: var(--muted-color);
1467 | padding: calc(var(--form-element-spacing-vertical) * 0.5) 0;
1468 | border: 0;
1469 | border-radius: 0;
1470 | background: none;
1471 | }
1472 | [type=file]::file-selector-button {
1473 | --background-color: var(--secondary);
1474 | --border-color: var(--secondary);
1475 | --color: var(--secondary-inverse);
1476 | margin-right: calc(var(--spacing) / 2);
1477 | margin-left: 0;
1478 | -webkit-margin-start: 0;
1479 | margin-inline-start: 0;
1480 | -webkit-margin-end: calc(var(--spacing) / 2);
1481 | margin-inline-end: calc(var(--spacing) / 2);
1482 | padding: calc(var(--form-element-spacing-vertical) * 0.5) calc(var(--form-element-spacing-horizontal) * 0.5);
1483 | border: var(--border-width) solid var(--border-color);
1484 | border-radius: var(--border-radius);
1485 | outline: none;
1486 | background-color: var(--background-color);
1487 | box-shadow: var(--box-shadow);
1488 | color: var(--color);
1489 | font-weight: var(--font-weight);
1490 | font-size: 1rem;
1491 | line-height: var(--line-height);
1492 | text-align: center;
1493 | cursor: pointer;
1494 | transition: background-color var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition);
1495 | }
1496 | [type=file]::file-selector-button:is(:hover, :active, :focus) {
1497 | --background-color: var(--secondary-hover);
1498 | --border-color: var(--secondary-hover);
1499 | }
1500 | [type=file]::-webkit-file-upload-button {
1501 | --background-color: var(--secondary);
1502 | --border-color: var(--secondary);
1503 | --color: var(--secondary-inverse);
1504 | margin-right: calc(var(--spacing) / 2);
1505 | margin-left: 0;
1506 | -webkit-margin-start: 0;
1507 | margin-inline-start: 0;
1508 | -webkit-margin-end: calc(var(--spacing) / 2);
1509 | margin-inline-end: calc(var(--spacing) / 2);
1510 | padding: calc(var(--form-element-spacing-vertical) * 0.5) calc(var(--form-element-spacing-horizontal) * 0.5);
1511 | border: var(--border-width) solid var(--border-color);
1512 | border-radius: var(--border-radius);
1513 | outline: none;
1514 | background-color: var(--background-color);
1515 | box-shadow: var(--box-shadow);
1516 | color: var(--color);
1517 | font-weight: var(--font-weight);
1518 | font-size: 1rem;
1519 | line-height: var(--line-height);
1520 | text-align: center;
1521 | cursor: pointer;
1522 | -webkit-transition: background-color var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition);
1523 | transition: background-color var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition);
1524 | }
1525 | [type=file]::-webkit-file-upload-button:is(:hover, :active, :focus) {
1526 | --background-color: var(--secondary-hover);
1527 | --border-color: var(--secondary-hover);
1528 | }
1529 | [type=file]::-ms-browse {
1530 | --background-color: var(--secondary);
1531 | --border-color: var(--secondary);
1532 | --color: var(--secondary-inverse);
1533 | margin-right: calc(var(--spacing) / 2);
1534 | margin-left: 0;
1535 | margin-inline-start: 0;
1536 | margin-inline-end: calc(var(--spacing) / 2);
1537 | padding: calc(var(--form-element-spacing-vertical) * 0.5) calc(var(--form-element-spacing-horizontal) * 0.5);
1538 | border: var(--border-width) solid var(--border-color);
1539 | border-radius: var(--border-radius);
1540 | outline: none;
1541 | background-color: var(--background-color);
1542 | box-shadow: var(--box-shadow);
1543 | color: var(--color);
1544 | font-weight: var(--font-weight);
1545 | font-size: 1rem;
1546 | line-height: var(--line-height);
1547 | text-align: center;
1548 | cursor: pointer;
1549 | -ms-transition: background-color var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition);
1550 | transition: background-color var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition);
1551 | }
1552 | [type=file]::-ms-browse:is(:hover, :active, :focus) {
1553 | --background-color: var(--secondary-hover);
1554 | --border-color: var(--secondary-hover);
1555 | }
1556 |
1557 | [type=range] {
1558 | -webkit-appearance: none;
1559 | -moz-appearance: none;
1560 | appearance: none;
1561 | width: 100%;
1562 | height: 1.25rem;
1563 | background: none;
1564 | }
1565 | [type=range]::-webkit-slider-runnable-track {
1566 | width: 100%;
1567 | height: 0.25rem;
1568 | border-radius: var(--border-radius);
1569 | background-color: var(--range-border-color);
1570 | -webkit-transition: background-color var(--transition), box-shadow var(--transition);
1571 | transition: background-color var(--transition), box-shadow var(--transition);
1572 | }
1573 | [type=range]::-moz-range-track {
1574 | width: 100%;
1575 | height: 0.25rem;
1576 | border-radius: var(--border-radius);
1577 | background-color: var(--range-border-color);
1578 | -moz-transition: background-color var(--transition), box-shadow var(--transition);
1579 | transition: background-color var(--transition), box-shadow var(--transition);
1580 | }
1581 | [type=range]::-ms-track {
1582 | width: 100%;
1583 | height: 0.25rem;
1584 | border-radius: var(--border-radius);
1585 | background-color: var(--range-border-color);
1586 | -ms-transition: background-color var(--transition), box-shadow var(--transition);
1587 | transition: background-color var(--transition), box-shadow var(--transition);
1588 | }
1589 | [type=range]::-webkit-slider-thumb {
1590 | -webkit-appearance: none;
1591 | width: 1.25rem;
1592 | height: 1.25rem;
1593 | margin-top: -0.5rem;
1594 | border: 2px solid var(--range-thumb-border-color);
1595 | border-radius: 50%;
1596 | background-color: var(--range-thumb-color);
1597 | cursor: pointer;
1598 | -webkit-transition: background-color var(--transition), transform var(--transition);
1599 | transition: background-color var(--transition), transform var(--transition);
1600 | }
1601 | [type=range]::-moz-range-thumb {
1602 | -webkit-appearance: none;
1603 | width: 1.25rem;
1604 | height: 1.25rem;
1605 | margin-top: -0.5rem;
1606 | border: 2px solid var(--range-thumb-border-color);
1607 | border-radius: 50%;
1608 | background-color: var(--range-thumb-color);
1609 | cursor: pointer;
1610 | -moz-transition: background-color var(--transition), transform var(--transition);
1611 | transition: background-color var(--transition), transform var(--transition);
1612 | }
1613 | [type=range]::-ms-thumb {
1614 | -webkit-appearance: none;
1615 | width: 1.25rem;
1616 | height: 1.25rem;
1617 | margin-top: -0.5rem;
1618 | border: 2px solid var(--range-thumb-border-color);
1619 | border-radius: 50%;
1620 | background-color: var(--range-thumb-color);
1621 | cursor: pointer;
1622 | -ms-transition: background-color var(--transition), transform var(--transition);
1623 | transition: background-color var(--transition), transform var(--transition);
1624 | }
1625 | [type=range]:hover, [type=range]:focus {
1626 | --range-border-color: var(--range-active-border-color);
1627 | --range-thumb-color: var(--range-thumb-hover-color);
1628 | }
1629 | [type=range]:active {
1630 | --range-thumb-color: var(--range-thumb-active-color);
1631 | }
1632 | [type=range]:active::-webkit-slider-thumb {
1633 | transform: scale(1.25);
1634 | }
1635 | [type=range]:active::-moz-range-thumb {
1636 | transform: scale(1.25);
1637 | }
1638 | [type=range]:active::-ms-thumb {
1639 | transform: scale(1.25);
1640 | }
1641 |
1642 | input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search] {
1643 | -webkit-padding-start: calc(var(--form-element-spacing-horizontal) + 1.75rem);
1644 | padding-inline-start: calc(var(--form-element-spacing-horizontal) + 1.75rem);
1645 | border-radius: 5rem;
1646 | background-image: var(--icon-search);
1647 | background-position: center left 1.125rem;
1648 | background-size: 1rem auto;
1649 | background-repeat: no-repeat;
1650 | }
1651 | input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid] {
1652 | -webkit-padding-start: calc(var(--form-element-spacing-horizontal) + 1.75rem) !important;
1653 | padding-inline-start: calc(var(--form-element-spacing-horizontal) + 1.75rem) !important;
1654 | background-position: center left 1.125rem, center right 0.75rem;
1655 | }
1656 | input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid=false] {
1657 | background-image: var(--icon-search), var(--icon-valid);
1658 | }
1659 | input:not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid=true] {
1660 | background-image: var(--icon-search), var(--icon-invalid);
1661 | }
1662 |
1663 | [type=search]::-webkit-search-cancel-button {
1664 | -webkit-appearance: none;
1665 | display: none;
1666 | }
1667 |
1668 | [dir=rtl] :where(input):not([type=checkbox], [type=radio], [type=range], [type=file])[type=search] {
1669 | background-position: center right 1.125rem;
1670 | }
1671 | [dir=rtl] :where(input):not([type=checkbox], [type=radio], [type=range], [type=file])[type=search][aria-invalid] {
1672 | background-position: center right 1.125rem, center left 0.75rem;
1673 | }
1674 |
1675 | /**
1676 | * Table
1677 | */
1678 | :where(table) {
1679 | width: 100%;
1680 | border-collapse: collapse;
1681 | border-spacing: 0;
1682 | text-indent: 0;
1683 | }
1684 |
1685 | th,
1686 | td {
1687 | padding: calc(var(--spacing) / 2) var(--spacing);
1688 | border-bottom: var(--border-width) solid var(--table-border-color);
1689 | color: var(--color);
1690 | font-weight: var(--font-weight);
1691 | font-size: var(--font-size);
1692 | text-align: left;
1693 | text-align: start;
1694 | }
1695 |
1696 | tfoot th,
1697 | tfoot td {
1698 | border-top: var(--border-width) solid var(--table-border-color);
1699 | border-bottom: 0;
1700 | }
1701 |
1702 | table[role=grid] tbody tr:nth-child(odd) {
1703 | background-color: var(--table-row-stripped-background-color);
1704 | }
1705 |
1706 | /**
1707 | * Code
1708 | */
1709 | pre,
1710 | code,
1711 | kbd,
1712 | samp {
1713 | font-size: 0.875em;
1714 | font-family: var(--font-family);
1715 | }
1716 |
1717 | pre {
1718 | -ms-overflow-style: scrollbar;
1719 | overflow: auto;
1720 | }
1721 |
1722 | pre,
1723 | code,
1724 | kbd {
1725 | border-radius: var(--border-radius);
1726 | background: var(--code-background-color);
1727 | color: var(--code-color);
1728 | font-weight: var(--font-weight);
1729 | line-height: initial;
1730 | }
1731 |
1732 | code,
1733 | kbd {
1734 | display: inline-block;
1735 | padding: 0.375rem 0.5rem;
1736 | }
1737 |
1738 | pre {
1739 | display: block;
1740 | margin-bottom: var(--spacing);
1741 | overflow-x: auto;
1742 | }
1743 | pre > code {
1744 | display: block;
1745 | padding: var(--spacing);
1746 | background: none;
1747 | font-size: 14px;
1748 | line-height: var(--line-height);
1749 | }
1750 |
1751 | code b {
1752 | color: var(--code-tag-color);
1753 | font-weight: var(--font-weight);
1754 | }
1755 | code i {
1756 | color: var(--code-property-color);
1757 | font-style: normal;
1758 | }
1759 | code u {
1760 | color: var(--code-value-color);
1761 | text-decoration: none;
1762 | }
1763 | code em {
1764 | color: var(--code-comment-color);
1765 | font-style: normal;
1766 | }
1767 |
1768 | kbd {
1769 | background-color: var(--code-kbd-background-color);
1770 | color: var(--code-kbd-color);
1771 | vertical-align: baseline;
1772 | }
1773 |
1774 | /**
1775 | * Miscs
1776 | */
1777 | hr {
1778 | height: 0;
1779 | border: 0;
1780 | border-top: 1px solid var(--muted-border-color);
1781 | color: inherit;
1782 | }
1783 |
1784 | [hidden],
1785 | template {
1786 | display: none !important;
1787 | }
1788 |
1789 | canvas {
1790 | display: inline-block;
1791 | }
1792 |
1793 | /**
1794 | * Accordion ()
1795 | */
1796 | details {
1797 | display: block;
1798 | margin-bottom: var(--spacing);
1799 | padding-bottom: var(--spacing);
1800 | border-bottom: var(--border-width) solid var(--accordion-border-color);
1801 | }
1802 | details summary {
1803 | line-height: 1rem;
1804 | list-style-type: none;
1805 | cursor: pointer;
1806 | transition: color var(--transition);
1807 | }
1808 | details summary:not([role]) {
1809 | color: var(--accordion-close-summary-color);
1810 | }
1811 | details summary::-webkit-details-marker {
1812 | display: none;
1813 | }
1814 | details summary::marker {
1815 | display: none;
1816 | }
1817 | details summary::-moz-list-bullet {
1818 | list-style-type: none;
1819 | }
1820 | details summary::after {
1821 | display: block;
1822 | width: 1rem;
1823 | height: 1rem;
1824 | -webkit-margin-start: calc(var(--spacing, 1rem) * 0.5);
1825 | margin-inline-start: calc(var(--spacing, 1rem) * 0.5);
1826 | float: right;
1827 | transform: rotate(-90deg);
1828 | background-image: var(--icon-chevron);
1829 | background-position: right center;
1830 | background-size: 1rem auto;
1831 | background-repeat: no-repeat;
1832 | content: "";
1833 | transition: transform var(--transition);
1834 | }
1835 | details summary:focus {
1836 | outline: none;
1837 | }
1838 | details summary:focus:not([role=button]) {
1839 | color: var(--accordion-active-summary-color);
1840 | }
1841 | details summary[role=button] {
1842 | width: 100%;
1843 | text-align: left;
1844 | }
1845 | details summary[role=button]::after {
1846 | height: calc(1rem * var(--line-height, 1.5));
1847 | background-image: var(--icon-chevron-button);
1848 | }
1849 | details summary[role=button]:not(.outline).contrast::after {
1850 | background-image: var(--icon-chevron-button-inverse);
1851 | }
1852 | details[open] > summary {
1853 | margin-bottom: calc(var(--spacing));
1854 | }
1855 | details[open] > summary:not([role]):not(:focus) {
1856 | color: var(--accordion-open-summary-color);
1857 | }
1858 | details[open] > summary::after {
1859 | transform: rotate(0);
1860 | }
1861 |
1862 | [dir=rtl] details summary {
1863 | text-align: right;
1864 | }
1865 | [dir=rtl] details summary::after {
1866 | float: left;
1867 | background-position: left center;
1868 | }
1869 |
1870 | /**
1871 | * Card ()
1872 | */
1873 | article {
1874 | margin: var(--block-spacing-vertical) 0;
1875 | padding: var(--block-spacing-vertical) var(--block-spacing-horizontal);
1876 | border-radius: var(--border-radius);
1877 | background: var(--card-background-color);
1878 | box-shadow: var(--card-box-shadow);
1879 | }
1880 | article > header,
1881 | article > footer {
1882 | margin-right: calc(var(--block-spacing-horizontal) * -1);
1883 | margin-left: calc(var(--block-spacing-horizontal) * -1);
1884 | padding: calc(var(--block-spacing-vertical) * 0.66) var(--block-spacing-horizontal);
1885 | background-color: var(--card-sectionning-background-color);
1886 | }
1887 | article > header {
1888 | margin-top: calc(var(--block-spacing-vertical) * -1);
1889 | margin-bottom: var(--block-spacing-vertical);
1890 | border-bottom: var(--border-width) solid var(--card-border-color);
1891 | border-top-right-radius: var(--border-radius);
1892 | border-top-left-radius: var(--border-radius);
1893 | }
1894 | article > footer {
1895 | margin-top: var(--block-spacing-vertical);
1896 | margin-bottom: calc(var(--block-spacing-vertical) * -1);
1897 | border-top: var(--border-width) solid var(--card-border-color);
1898 | border-bottom-right-radius: var(--border-radius);
1899 | border-bottom-left-radius: var(--border-radius);
1900 | }
1901 |
1902 | /**
1903 | * Modal ()
1904 | */
1905 | :root {
1906 | --scrollbar-width: 0px;
1907 | }
1908 |
1909 | dialog {
1910 | display: flex;
1911 | z-index: 999;
1912 | position: fixed;
1913 | top: 0;
1914 | right: 0;
1915 | bottom: 0;
1916 | left: 0;
1917 | align-items: center;
1918 | justify-content: center;
1919 | width: inherit;
1920 | min-width: 100%;
1921 | height: inherit;
1922 | min-height: 100%;
1923 | padding: var(--spacing);
1924 | border: 0;
1925 | -webkit-backdrop-filter: var(--modal-overlay-backdrop-filter);
1926 | backdrop-filter: var(--modal-overlay-backdrop-filter);
1927 | background-color: var(--modal-overlay-background-color);
1928 | color: var(--color);
1929 | }
1930 | dialog article {
1931 | max-height: calc(100vh - var(--spacing) * 2);
1932 | overflow: auto;
1933 | }
1934 | @media (min-width: 576px) {
1935 | dialog article {
1936 | max-width: 510px;
1937 | }
1938 | }
1939 | @media (min-width: 768px) {
1940 | dialog article {
1941 | max-width: 700px;
1942 | }
1943 | }
1944 | dialog article > header,
1945 | dialog article > footer {
1946 | padding: calc(var(--block-spacing-vertical) * 0.5) var(--block-spacing-horizontal);
1947 | }
1948 | dialog article > header .close {
1949 | margin: 0;
1950 | margin-left: var(--spacing);
1951 | float: right;
1952 | }
1953 | dialog article > footer {
1954 | text-align: right;
1955 | }
1956 | dialog article > footer [role=button] {
1957 | margin-bottom: 0;
1958 | }
1959 | dialog article > footer [role=button]:not(:first-of-type) {
1960 | margin-left: calc(var(--spacing) * 0.5);
1961 | }
1962 | dialog article p:last-of-type {
1963 | margin: 0;
1964 | }
1965 | dialog article .close {
1966 | display: block;
1967 | width: 1rem;
1968 | height: 1rem;
1969 | margin-top: calc(var(--block-spacing-vertical) * -0.5);
1970 | margin-bottom: var(--typography-spacing-vertical);
1971 | margin-left: auto;
1972 | background-image: var(--icon-close);
1973 | background-position: center;
1974 | background-size: auto 1rem;
1975 | background-repeat: no-repeat;
1976 | opacity: 0.5;
1977 | transition: opacity var(--transition);
1978 | }
1979 | dialog article .close:is([aria-current], :hover, :active, :focus) {
1980 | opacity: 1;
1981 | }
1982 | dialog:not([open]), dialog[open=false] {
1983 | display: none;
1984 | }
1985 |
1986 | .modal-is-open {
1987 | padding-right: var(--scrollbar-width, 0px);
1988 | overflow: hidden;
1989 | pointer-events: none;
1990 | touch-action: none;
1991 | }
1992 | .modal-is-open dialog {
1993 | pointer-events: auto;
1994 | }
1995 |
1996 | :where(.modal-is-opening, .modal-is-closing) dialog,
1997 | :where(.modal-is-opening, .modal-is-closing) dialog > article {
1998 | animation-duration: 0.2s;
1999 | animation-timing-function: ease-in-out;
2000 | animation-fill-mode: both;
2001 | }
2002 | :where(.modal-is-opening, .modal-is-closing) dialog {
2003 | animation-duration: 0.8s;
2004 | animation-name: modal-overlay;
2005 | }
2006 | :where(.modal-is-opening, .modal-is-closing) dialog > article {
2007 | animation-delay: 0.2s;
2008 | animation-name: modal;
2009 | }
2010 |
2011 | .modal-is-closing dialog,
2012 | .modal-is-closing dialog > article {
2013 | animation-delay: 0s;
2014 | animation-direction: reverse;
2015 | }
2016 |
2017 | @keyframes modal-overlay {
2018 | from {
2019 | -webkit-backdrop-filter: none;
2020 | backdrop-filter: none;
2021 | background-color: transparent;
2022 | }
2023 | }
2024 | @keyframes modal {
2025 | from {
2026 | transform: translateY(-100%);
2027 | opacity: 0;
2028 | }
2029 | }
2030 | /**
2031 | * Nav
2032 | */
2033 | :where(nav li)::before {
2034 | float: left;
2035 | content: "";
2036 | }
2037 |
2038 | nav,
2039 | nav ul {
2040 | display: flex;
2041 | }
2042 |
2043 | nav {
2044 | justify-content: space-between;
2045 | }
2046 | nav ol,
2047 | nav ul {
2048 | align-items: center;
2049 | margin-bottom: 0;
2050 | padding: 0;
2051 | list-style: none;
2052 | }
2053 | nav ol:first-of-type,
2054 | nav ul:first-of-type {
2055 | margin-left: calc(var(--nav-element-spacing-horizontal) * -1);
2056 | }
2057 | nav ol:last-of-type,
2058 | nav ul:last-of-type {
2059 | margin-right: calc(var(--nav-element-spacing-horizontal) * -1);
2060 | }
2061 | nav li {
2062 | display: inline-block;
2063 | margin: 0;
2064 | padding: var(--nav-element-spacing-vertical) var(--nav-element-spacing-horizontal);
2065 | }
2066 | nav li > * {
2067 | --spacing: 0;
2068 | }
2069 | nav :where(a, [role=link]) {
2070 | display: inline-block;
2071 | margin: calc(var(--nav-link-spacing-vertical) * -1) calc(var(--nav-link-spacing-horizontal) * -1);
2072 | padding: var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal);
2073 | border-radius: var(--border-radius);
2074 | text-decoration: none;
2075 | }
2076 | nav :where(a, [role=link]):is([aria-current], :hover, :active, :focus) {
2077 | text-decoration: none;
2078 | }
2079 | nav[aria-label=breadcrumb] {
2080 | align-items: center;
2081 | justify-content: start;
2082 | }
2083 | nav[aria-label=breadcrumb] ul li:not(:first-child) {
2084 | -webkit-margin-start: var(--nav-link-spacing-horizontal);
2085 | margin-inline-start: var(--nav-link-spacing-horizontal);
2086 | }
2087 | nav[aria-label=breadcrumb] ul li:not(:last-child) ::after {
2088 | position: absolute;
2089 | width: calc(var(--nav-link-spacing-horizontal) * 2);
2090 | -webkit-margin-start: calc(var(--nav-link-spacing-horizontal) / 2);
2091 | margin-inline-start: calc(var(--nav-link-spacing-horizontal) / 2);
2092 | content: "/";
2093 | color: var(--muted-color);
2094 | text-align: center;
2095 | }
2096 | nav[aria-label=breadcrumb] a[aria-current] {
2097 | background-color: transparent;
2098 | color: inherit;
2099 | text-decoration: none;
2100 | pointer-events: none;
2101 | }
2102 | nav [role=button] {
2103 | margin-right: inherit;
2104 | margin-left: inherit;
2105 | padding: var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal);
2106 | }
2107 |
2108 | aside nav,
2109 | aside ol,
2110 | aside ul,
2111 | aside li {
2112 | display: block;
2113 | }
2114 | aside li {
2115 | padding: calc(var(--nav-element-spacing-vertical) * 0.5) var(--nav-element-spacing-horizontal);
2116 | }
2117 | aside li a {
2118 | display: block;
2119 | }
2120 | aside li [role=button] {
2121 | margin: inherit;
2122 | }
2123 |
2124 | [dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after {
2125 | content: "\\";
2126 | }
2127 |
2128 | /**
2129 | * Progress
2130 | */
2131 | progress {
2132 | display: inline-block;
2133 | vertical-align: baseline;
2134 | }
2135 |
2136 | progress {
2137 | -webkit-appearance: none;
2138 | -moz-appearance: none;
2139 | display: inline-block;
2140 | appearance: none;
2141 | width: 100%;
2142 | height: 0.5rem;
2143 | margin-bottom: calc(var(--spacing) * 0.5);
2144 | overflow: hidden;
2145 | border: 0;
2146 | border-radius: var(--border-radius);
2147 | background-color: var(--progress-background-color);
2148 | color: var(--progress-color);
2149 | }
2150 | progress::-webkit-progress-bar {
2151 | border-radius: var(--border-radius);
2152 | background: none;
2153 | }
2154 | progress[value]::-webkit-progress-value {
2155 | background-color: var(--progress-color);
2156 | }
2157 | progress::-moz-progress-bar {
2158 | background-color: var(--progress-color);
2159 | }
2160 | @media (prefers-reduced-motion: no-preference) {
2161 | progress:indeterminate {
2162 | background: var(--progress-background-color) linear-gradient(to right, var(--progress-color) 30%, var(--progress-background-color) 30%) top left/150% 150% no-repeat;
2163 | animation: progress-indeterminate 1s linear infinite;
2164 | }
2165 | progress:indeterminate[value]::-webkit-progress-value {
2166 | background-color: transparent;
2167 | }
2168 | progress:indeterminate::-moz-progress-bar {
2169 | background-color: transparent;
2170 | }
2171 | }
2172 |
2173 | @media (prefers-reduced-motion: no-preference) {
2174 | [dir=rtl] progress:indeterminate {
2175 | animation-direction: reverse;
2176 | }
2177 | }
2178 |
2179 | @keyframes progress-indeterminate {
2180 | 0% {
2181 | background-position: 200% 0;
2182 | }
2183 | 100% {
2184 | background-position: -200% 0;
2185 | }
2186 | }
2187 | /**
2188 | * Dropdown ([role="list"])
2189 | */
2190 | details[role=list],
2191 | li[role=list] {
2192 | position: relative;
2193 | }
2194 |
2195 | details[role=list] summary + ul,
2196 | li[role=list] > ul {
2197 | display: flex;
2198 | z-index: 99;
2199 | position: absolute;
2200 | top: auto;
2201 | right: 0;
2202 | left: 0;
2203 | flex-direction: column;
2204 | margin: 0;
2205 | padding: 0;
2206 | border: var(--border-width) solid var(--dropdown-border-color);
2207 | border-radius: var(--border-radius);
2208 | border-top-right-radius: 0;
2209 | border-top-left-radius: 0;
2210 | background-color: var(--dropdown-background-color);
2211 | box-shadow: var(--card-box-shadow);
2212 | color: var(--dropdown-color);
2213 | white-space: nowrap;
2214 | }
2215 | details[role=list] summary + ul li,
2216 | li[role=list] > ul li {
2217 | width: 100%;
2218 | margin-bottom: 0;
2219 | padding: calc(var(--form-element-spacing-vertical) * 0.5) var(--form-element-spacing-horizontal);
2220 | list-style: none;
2221 | }
2222 | details[role=list] summary + ul li:first-of-type,
2223 | li[role=list] > ul li:first-of-type {
2224 | margin-top: calc(var(--form-element-spacing-vertical) * 0.5);
2225 | }
2226 | details[role=list] summary + ul li:last-of-type,
2227 | li[role=list] > ul li:last-of-type {
2228 | margin-bottom: calc(var(--form-element-spacing-vertical) * 0.5);
2229 | }
2230 | details[role=list] summary + ul li a,
2231 | li[role=list] > ul li a {
2232 | display: block;
2233 | margin: calc(var(--form-element-spacing-vertical) * -0.5) calc(var(--form-element-spacing-horizontal) * -1);
2234 | padding: calc(var(--form-element-spacing-vertical) * 0.5) var(--form-element-spacing-horizontal);
2235 | overflow: hidden;
2236 | color: var(--dropdown-color);
2237 | text-decoration: none;
2238 | text-overflow: ellipsis;
2239 | }
2240 | details[role=list] summary + ul li a:hover,
2241 | li[role=list] > ul li a:hover {
2242 | background-color: var(--dropdown-hover-background-color);
2243 | }
2244 |
2245 | details[role=list] summary::after,
2246 | li[role=list] > a::after {
2247 | display: block;
2248 | width: 1rem;
2249 | height: calc(1rem * var(--line-height, 1.5));
2250 | -webkit-margin-start: 0.5rem;
2251 | margin-inline-start: 0.5rem;
2252 | float: right;
2253 | transform: rotate(0deg);
2254 | background-position: right center;
2255 | background-size: 1rem auto;
2256 | background-repeat: no-repeat;
2257 | content: "";
2258 | }
2259 |
2260 | details[role=list] {
2261 | padding: 0;
2262 | border-bottom: none;
2263 | }
2264 | details[role=list] summary {
2265 | margin-bottom: 0;
2266 | }
2267 | details[role=list] summary:not([role]) {
2268 | height: calc(1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + var(--border-width) * 2);
2269 | padding: var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal);
2270 | border: var(--border-width) solid var(--form-element-border-color);
2271 | border-radius: var(--border-radius);
2272 | background-color: var(--form-element-background-color);
2273 | color: var(--form-element-placeholder-color);
2274 | line-height: inherit;
2275 | cursor: pointer;
2276 | transition: background-color var(--transition), border-color var(--transition), color var(--transition), box-shadow var(--transition);
2277 | }
2278 | details[role=list] summary:not([role]):active, details[role=list] summary:not([role]):focus {
2279 | border-color: var(--form-element-active-border-color);
2280 | background-color: var(--form-element-active-background-color);
2281 | }
2282 | details[role=list] summary:not([role]):focus {
2283 | box-shadow: 0 0 0 var(--outline-width) var(--form-element-focus-color);
2284 | }
2285 | details[role=list][open] summary {
2286 | border-bottom-right-radius: 0;
2287 | border-bottom-left-radius: 0;
2288 | }
2289 | details[role=list][open] summary::before {
2290 | display: block;
2291 | z-index: 1;
2292 | position: fixed;
2293 | top: 0;
2294 | right: 0;
2295 | bottom: 0;
2296 | left: 0;
2297 | background: none;
2298 | content: "";
2299 | cursor: default;
2300 | }
2301 |
2302 | nav details[role=list] summary,
2303 | nav li[role=list] a {
2304 | display: flex;
2305 | direction: ltr;
2306 | }
2307 |
2308 | nav details[role=list] summary + ul,
2309 | nav li[role=list] > ul {
2310 | min-width: -moz-fit-content;
2311 | min-width: fit-content;
2312 | border-radius: var(--border-radius);
2313 | }
2314 | nav details[role=list] summary + ul li a,
2315 | nav li[role=list] > ul li a {
2316 | border-radius: 0;
2317 | }
2318 |
2319 | nav details[role=list] summary,
2320 | nav details[role=list] summary:not([role]) {
2321 | height: auto;
2322 | padding: var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal);
2323 | }
2324 | nav details[role=list][open] summary {
2325 | border-radius: var(--border-radius);
2326 | }
2327 | nav details[role=list] summary + ul {
2328 | margin-top: var(--outline-width);
2329 | -webkit-margin-start: 0;
2330 | margin-inline-start: 0;
2331 | }
2332 | nav details[role=list] summary[role=link] {
2333 | margin-bottom: calc(var(--nav-link-spacing-vertical) * -1);
2334 | line-height: var(--line-height);
2335 | }
2336 | nav details[role=list] summary[role=link] + ul {
2337 | margin-top: calc(var(--nav-link-spacing-vertical) + var(--outline-width));
2338 | -webkit-margin-start: calc(var(--nav-link-spacing-horizontal) * -1);
2339 | margin-inline-start: calc(var(--nav-link-spacing-horizontal) * -1);
2340 | }
2341 |
2342 | li[role=list]:hover > ul,
2343 | li[role=list] a:active ~ ul,
2344 | li[role=list] a:focus ~ ul {
2345 | display: flex;
2346 | }
2347 | li[role=list] > ul {
2348 | display: none;
2349 | margin-top: calc(var(--nav-link-spacing-vertical) + var(--outline-width));
2350 | -webkit-margin-start: calc(var(--nav-element-spacing-horizontal) - var(--nav-link-spacing-horizontal));
2351 | margin-inline-start: calc(var(--nav-element-spacing-horizontal) - var(--nav-link-spacing-horizontal));
2352 | }
2353 | li[role=list] > a::after {
2354 | background-image: var(--icon-chevron);
2355 | }
2356 |
2357 | label > details[role=list] {
2358 | margin-top: calc(var(--spacing) * 0.25);
2359 | margin-bottom: var(--spacing);
2360 | }
2361 |
2362 | /**
2363 | * Loading ([aria-busy=true])
2364 | */
2365 | [aria-busy=true] {
2366 | cursor: progress;
2367 | }
2368 |
2369 | [aria-busy=true]:not(input, select, textarea, html)::before {
2370 | display: inline-block;
2371 | width: 1em;
2372 | height: 1em;
2373 | border: 0.1875em solid currentColor;
2374 | border-radius: 1em;
2375 | border-right-color: transparent;
2376 | content: "";
2377 | vertical-align: text-bottom;
2378 | vertical-align: -0.125em;
2379 | animation: spinner 0.75s linear infinite;
2380 | opacity: var(--loading-spinner-opacity);
2381 | }
2382 | [aria-busy=true]:not(input, select, textarea, html):not(:empty)::before {
2383 | margin-right: calc(var(--spacing) * 0.5);
2384 | margin-left: 0;
2385 | -webkit-margin-start: 0;
2386 | margin-inline-start: 0;
2387 | -webkit-margin-end: calc(var(--spacing) * 0.5);
2388 | margin-inline-end: calc(var(--spacing) * 0.5);
2389 | }
2390 | [aria-busy=true]:not(input, select, textarea, html):empty {
2391 | text-align: center;
2392 | }
2393 |
2394 | button[aria-busy=true],
2395 | input[type=submit][aria-busy=true],
2396 | input[type=button][aria-busy=true],
2397 | input[type=reset][aria-busy=true],
2398 | a[aria-busy=true] {
2399 | pointer-events: none;
2400 | }
2401 |
2402 | @keyframes spinner {
2403 | to {
2404 | transform: rotate(360deg);
2405 | }
2406 | }
2407 | /**
2408 | * Tooltip ([data-tooltip])
2409 | */
2410 | [data-tooltip] {
2411 | position: relative;
2412 | }
2413 | [data-tooltip]:not(a, button, input) {
2414 | border-bottom: 1px dotted;
2415 | text-decoration: none;
2416 | cursor: help;
2417 | }
2418 | [data-tooltip][data-placement=top]::before, [data-tooltip][data-placement=top]::after, [data-tooltip]::before, [data-tooltip]::after {
2419 | display: block;
2420 | z-index: 99;
2421 | position: absolute;
2422 | bottom: 100%;
2423 | left: 50%;
2424 | padding: 0.25rem 0.5rem;
2425 | overflow: hidden;
2426 | transform: translate(-50%, -0.25rem);
2427 | border-radius: var(--border-radius);
2428 | background: var(--tooltip-background-color);
2429 | content: attr(data-tooltip);
2430 | color: var(--tooltip-color);
2431 | font-style: normal;
2432 | font-weight: var(--font-weight);
2433 | font-size: 0.875rem;
2434 | text-decoration: none;
2435 | text-overflow: ellipsis;
2436 | white-space: nowrap;
2437 | opacity: 0;
2438 | pointer-events: none;
2439 | }
2440 | [data-tooltip][data-placement=top]::after, [data-tooltip]::after {
2441 | padding: 0;
2442 | transform: translate(-50%, 0rem);
2443 | border-top: 0.3rem solid;
2444 | border-right: 0.3rem solid transparent;
2445 | border-left: 0.3rem solid transparent;
2446 | border-radius: 0;
2447 | background-color: transparent;
2448 | content: "";
2449 | color: var(--tooltip-background-color);
2450 | }
2451 | [data-tooltip][data-placement=bottom]::before, [data-tooltip][data-placement=bottom]::after {
2452 | top: 100%;
2453 | bottom: auto;
2454 | transform: translate(-50%, 0.25rem);
2455 | }
2456 | [data-tooltip][data-placement=bottom]:after {
2457 | transform: translate(-50%, -0.3rem);
2458 | border: 0.3rem solid transparent;
2459 | border-bottom: 0.3rem solid;
2460 | }
2461 | [data-tooltip][data-placement=left]::before, [data-tooltip][data-placement=left]::after {
2462 | top: 50%;
2463 | right: 100%;
2464 | bottom: auto;
2465 | left: auto;
2466 | transform: translate(-0.25rem, -50%);
2467 | }
2468 | [data-tooltip][data-placement=left]:after {
2469 | transform: translate(0.3rem, -50%);
2470 | border: 0.3rem solid transparent;
2471 | border-left: 0.3rem solid;
2472 | }
2473 | [data-tooltip][data-placement=right]::before, [data-tooltip][data-placement=right]::after {
2474 | top: 50%;
2475 | right: auto;
2476 | bottom: auto;
2477 | left: 100%;
2478 | transform: translate(0.25rem, -50%);
2479 | }
2480 | [data-tooltip][data-placement=right]:after {
2481 | transform: translate(-0.3rem, -50%);
2482 | border: 0.3rem solid transparent;
2483 | border-right: 0.3rem solid;
2484 | }
2485 | [data-tooltip]:focus::before, [data-tooltip]:focus::after, [data-tooltip]:hover::before, [data-tooltip]:hover::after {
2486 | opacity: 1;
2487 | }
2488 | @media (hover: hover) and (pointer: fine) {
2489 | [data-tooltip][data-placement=bottom]:focus::before, [data-tooltip][data-placement=bottom]:focus::after, [data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::before, [data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::after, [data-tooltip]:hover::before, [data-tooltip]:hover::after {
2490 | animation-duration: 0.2s;
2491 | animation-name: tooltip-slide-top;
2492 | }
2493 | [data-tooltip][data-placement=bottom]:focus::after, [data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::after, [data-tooltip]:hover::after {
2494 | animation-name: tooltip-caret-slide-top;
2495 | }
2496 | [data-tooltip][data-placement=bottom]:focus::before, [data-tooltip][data-placement=bottom]:focus::after, [data-tooltip][data-placement=bottom]:hover::before, [data-tooltip][data-placement=bottom]:hover::after {
2497 | animation-duration: 0.2s;
2498 | animation-name: tooltip-slide-bottom;
2499 | }
2500 | [data-tooltip][data-placement=bottom]:focus::after, [data-tooltip][data-placement=bottom]:hover::after {
2501 | animation-name: tooltip-caret-slide-bottom;
2502 | }
2503 | [data-tooltip][data-placement=left]:focus::before, [data-tooltip][data-placement=left]:focus::after, [data-tooltip][data-placement=left]:hover::before, [data-tooltip][data-placement=left]:hover::after {
2504 | animation-duration: 0.2s;
2505 | animation-name: tooltip-slide-left;
2506 | }
2507 | [data-tooltip][data-placement=left]:focus::after, [data-tooltip][data-placement=left]:hover::after {
2508 | animation-name: tooltip-caret-slide-left;
2509 | }
2510 | [data-tooltip][data-placement=right]:focus::before, [data-tooltip][data-placement=right]:focus::after, [data-tooltip][data-placement=right]:hover::before, [data-tooltip][data-placement=right]:hover::after {
2511 | animation-duration: 0.2s;
2512 | animation-name: tooltip-slide-right;
2513 | }
2514 | [data-tooltip][data-placement=right]:focus::after, [data-tooltip][data-placement=right]:hover::after {
2515 | animation-name: tooltip-caret-slide-right;
2516 | }
2517 | }
2518 | @keyframes tooltip-slide-top {
2519 | from {
2520 | transform: translate(-50%, 0.75rem);
2521 | opacity: 0;
2522 | }
2523 | to {
2524 | transform: translate(-50%, -0.25rem);
2525 | opacity: 1;
2526 | }
2527 | }
2528 | @keyframes tooltip-caret-slide-top {
2529 | from {
2530 | opacity: 0;
2531 | }
2532 | 50% {
2533 | transform: translate(-50%, -0.25rem);
2534 | opacity: 0;
2535 | }
2536 | to {
2537 | transform: translate(-50%, 0rem);
2538 | opacity: 1;
2539 | }
2540 | }
2541 | @keyframes tooltip-slide-bottom {
2542 | from {
2543 | transform: translate(-50%, -0.75rem);
2544 | opacity: 0;
2545 | }
2546 | to {
2547 | transform: translate(-50%, 0.25rem);
2548 | opacity: 1;
2549 | }
2550 | }
2551 | @keyframes tooltip-caret-slide-bottom {
2552 | from {
2553 | opacity: 0;
2554 | }
2555 | 50% {
2556 | transform: translate(-50%, -0.5rem);
2557 | opacity: 0;
2558 | }
2559 | to {
2560 | transform: translate(-50%, -0.3rem);
2561 | opacity: 1;
2562 | }
2563 | }
2564 | @keyframes tooltip-slide-left {
2565 | from {
2566 | transform: translate(0.75rem, -50%);
2567 | opacity: 0;
2568 | }
2569 | to {
2570 | transform: translate(-0.25rem, -50%);
2571 | opacity: 1;
2572 | }
2573 | }
2574 | @keyframes tooltip-caret-slide-left {
2575 | from {
2576 | opacity: 0;
2577 | }
2578 | 50% {
2579 | transform: translate(0.05rem, -50%);
2580 | opacity: 0;
2581 | }
2582 | to {
2583 | transform: translate(0.3rem, -50%);
2584 | opacity: 1;
2585 | }
2586 | }
2587 | @keyframes tooltip-slide-right {
2588 | from {
2589 | transform: translate(-0.75rem, -50%);
2590 | opacity: 0;
2591 | }
2592 | to {
2593 | transform: translate(0.25rem, -50%);
2594 | opacity: 1;
2595 | }
2596 | }
2597 | @keyframes tooltip-caret-slide-right {
2598 | from {
2599 | opacity: 0;
2600 | }
2601 | 50% {
2602 | transform: translate(-0.05rem, -50%);
2603 | opacity: 0;
2604 | }
2605 | to {
2606 | transform: translate(-0.3rem, -50%);
2607 | opacity: 1;
2608 | }
2609 | }
2610 |
2611 | /**
2612 | * Accessibility & User interaction
2613 | */
2614 | [aria-controls] {
2615 | cursor: pointer;
2616 | }
2617 |
2618 | [aria-disabled=true],
2619 | [disabled] {
2620 | cursor: not-allowed;
2621 | }
2622 |
2623 | [aria-hidden=false][hidden] {
2624 | display: initial;
2625 | }
2626 |
2627 | [aria-hidden=false][hidden]:not(:focus) {
2628 | clip: rect(0, 0, 0, 0);
2629 | position: absolute;
2630 | }
2631 |
2632 | a,
2633 | area,
2634 | button,
2635 | input,
2636 | label,
2637 | select,
2638 | summary,
2639 | textarea,
2640 | [tabindex] {
2641 | -ms-touch-action: manipulation;
2642 | }
2643 |
2644 | [dir=rtl] {
2645 | direction: rtl;
2646 | }
2647 |
2648 | /**
2649 | * Reduce Motion Features
2650 | */
2651 | @media (prefers-reduced-motion: reduce) {
2652 | *:not([aria-busy=true]),
2653 | :not([aria-busy=true])::before,
2654 | :not([aria-busy=true])::after {
2655 | background-attachment: initial !important;
2656 | animation-duration: 1ms !important;
2657 | animation-delay: -1ms !important;
2658 | animation-iteration-count: 1 !important;
2659 | scroll-behavior: auto !important;
2660 | transition-delay: 0s !important;
2661 | transition-duration: 0s !important;
2662 | }
2663 | }
2664 |
--------------------------------------------------------------------------------