diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..631a13a --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Ruby + uses: actions/setup-ruby@v1 + with: + ruby-version: 3.0 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: 18 + + - name: Install dependencies + run: | + bin/setup + + - name: Run tests + run: | + bin/test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8e0a7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +/.bundle/ +/doc/ +/log/*.log +/pkg/ +/tmp/ +/test/dummy/db/*.sqlite3 +/test/dummy/db/*.sqlite3-* +/test/dummy/log/*.log +/test/dummy/storage/ +/test/dummy/tmp/ +.byebug_history +dist/ +.env +node_modules/ +/test/dummy/public/clapton diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..db9074f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,84 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hi@moeki.org. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, +available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..6471f60 --- /dev/null +++ b/Gemfile @@ -0,0 +1,19 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +# Specify your gem's dependencies in clapton.gemspec. +gemspec + +group :development do + gem 'sqlite3', '~> 1.4' + gem 'puma' + gem 'ruby-openai' + gem 'rspec' + gem 'rspec-rails' + gem 'minitest' + gem 'capybara' + gem 'selenium-webdriver' +end + +# To use a debugger +# gem 'byebug', group: [:development, :test] diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..b9c5785 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,292 @@ +PATH + remote: . + specs: + clapton (0.1.0) + capybara (~> 3) + execjs (~> 2) + listen (~> 3) + rails (~> 6.1.7, >= 6.1.7.8) + ruby2js (~> 5) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (6.1.7.8) + actionpack (= 6.1.7.8) + activesupport (= 6.1.7.8) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (6.1.7.8) + actionpack (= 6.1.7.8) + activejob (= 6.1.7.8) + activerecord (= 6.1.7.8) + activestorage (= 6.1.7.8) + activesupport (= 6.1.7.8) + mail (>= 2.7.1) + actionmailer (6.1.7.8) + actionpack (= 6.1.7.8) + actionview (= 6.1.7.8) + activejob (= 6.1.7.8) + activesupport (= 6.1.7.8) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (6.1.7.8) + actionview (= 6.1.7.8) + activesupport (= 6.1.7.8) + rack (~> 2.0, >= 2.0.9) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.1.7.8) + actionpack (= 6.1.7.8) + activerecord (= 6.1.7.8) + activestorage (= 6.1.7.8) + activesupport (= 6.1.7.8) + nokogiri (>= 1.8.5) + actionview (6.1.7.8) + activesupport (= 6.1.7.8) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (6.1.7.8) + activesupport (= 6.1.7.8) + globalid (>= 0.3.6) + activemodel (6.1.7.8) + activesupport (= 6.1.7.8) + activerecord (6.1.7.8) + activemodel (= 6.1.7.8) + activesupport (= 6.1.7.8) + activestorage (6.1.7.8) + actionpack (= 6.1.7.8) + activejob (= 6.1.7.8) + activerecord (= 6.1.7.8) + activesupport (= 6.1.7.8) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (6.1.7.8) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + ast (2.4.2) + base64 (0.2.0) + builder (3.3.0) + capybara (3.40.0) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.11) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) + concurrent-ruby (1.3.4) + crass (1.0.6) + date (3.3.4) + diff-lcs (1.5.1) + erubi (1.13.0) + event_stream_parser (1.0.0) + execjs (2.9.1) + faraday (2.12.0) + faraday-net_http (>= 2.0, < 3.4) + json + logger + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (3.3.0) + net-http + ffi (1.17.0-aarch64-linux-gnu) + ffi (1.17.0-aarch64-linux-musl) + ffi (1.17.0-arm-linux-gnu) + ffi (1.17.0-arm-linux-musl) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86-linux-gnu) + ffi (1.17.0-x86-linux-musl) + ffi (1.17.0-x86_64-darwin) + ffi (1.17.0-x86_64-linux-gnu) + ffi (1.17.0-x86_64-linux-musl) + globalid (1.2.1) + activesupport (>= 6.1) + i18n (1.14.6) + concurrent-ruby (~> 1.0) + json (2.7.2) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.1) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.8.1) + mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp + marcel (1.0.4) + matrix (0.4.2) + method_source (1.1.0) + mini_mime (1.1.5) + minitest (5.25.1) + multipart-post (2.4.1) + net-http (0.4.1) + uri + net-imap (0.4.16) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout + net-smtp (0.5.0) + net-protocol + nio4r (2.7.3) + nokogiri (1.16.7-aarch64-linux) + racc (~> 1.4) + nokogiri (1.16.7-arm-linux) + racc (~> 1.4) + nokogiri (1.16.7-arm64-darwin) + racc (~> 1.4) + nokogiri (1.16.7-x86-linux) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-darwin) + racc (~> 1.4) + nokogiri (1.16.7-x86_64-linux) + racc (~> 1.4) + parser (3.3.5.0) + ast (~> 2.4.1) + racc + public_suffix (6.0.1) + puma (6.4.3) + nio4r (~> 2.0) + racc (1.8.1) + rack (2.2.9) + rack-test (2.1.0) + rack (>= 1.3) + rails (6.1.7.8) + actioncable (= 6.1.7.8) + actionmailbox (= 6.1.7.8) + actionmailer (= 6.1.7.8) + actionpack (= 6.1.7.8) + actiontext (= 6.1.7.8) + actionview (= 6.1.7.8) + activejob (= 6.1.7.8) + activemodel (= 6.1.7.8) + activerecord (= 6.1.7.8) + activestorage (= 6.1.7.8) + activesupport (= 6.1.7.8) + bundler (>= 1.15.0) + railties (= 6.1.7.8) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (6.1.7.8) + actionpack (= 6.1.7.8) + activesupport (= 6.1.7.8) + method_source + rake (>= 12.2) + thor (~> 1.0) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + regexp_parser (2.1.1) + rexml (3.3.8) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.1) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-rails (6.1.5) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) + rspec-support (3.13.1) + ruby-openai (7.3.0) + event_stream_parser (>= 0.3.0, < 2.0.0) + faraday (>= 1) + faraday-multipart (>= 1) + ruby2js (5.1.2) + parser + regexp_parser (~> 2.1.1) + rubyzip (2.3.2) + selenium-webdriver (4.25.0) + base64 (~> 0.2) + logger (~> 1.4) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) + sprockets (4.2.1) + concurrent-ruby (~> 1.0) + rack (>= 2.2.4, < 4) + sprockets-rails (3.5.2) + actionpack (>= 6.1) + activesupport (>= 6.1) + sprockets (>= 3.0.0) + sqlite3 (1.7.3-aarch64-linux) + sqlite3 (1.7.3-arm-linux) + sqlite3 (1.7.3-arm64-darwin) + sqlite3 (1.7.3-x86-linux) + sqlite3 (1.7.3-x86_64-darwin) + sqlite3 (1.7.3-x86_64-linux) + thor (1.3.2) + timeout (0.4.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (0.13.1) + websocket (1.2.11) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) + zeitwerk (2.6.18) + +PLATFORMS + aarch64-linux + aarch64-linux-gnu + aarch64-linux-musl + arm-linux + arm-linux-gnu + arm-linux-musl + arm64-darwin + x86-linux + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + capybara + clapton! + minitest + puma + rspec + rspec-rails + ruby-openai + selenium-webdriver + sqlite3 (~> 1.4) + +BUNDLED WITH + 2.5.17 diff --git a/MIT-LICENSE b/MIT-LICENSE new file mode 100644 index 0000000..85f62e6 --- /dev/null +++ b/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright 2024 Moeki Kawakami + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..df1efe4 --- /dev/null +++ b/README.md @@ -0,0 +1,286 @@ +# Clapton +Clapton is a Ruby on Rails gem for building web apps with pure Ruby only (no JavaScript and no HTML templates). + +# Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'clapton' +``` + +And then execute: + + $ bundle install + +## Usage + +To use a Clapton component in your view: + +```ruby +# app/components/task_list_component.rb +class TaskListComponent < Clapton::Component + def render + @state.tasks.each do |task| + @root.add(TaskItemComponent.new(id: task[:id], title: task[:title], due: task[:due], done: task[:done])) + end + add_button = Clapton::Button.new + add_button.add(Clapton::Text.new("Add Task")) + add_button.add_action(:click, :TaskListState, :add_task) + @root.add(add_button) + @root.render + end +end + +``` + +```ruby +# app/components/task_item_component.rb +class TaskItemComponent < Clapton::Component + def render + button = Clapton::Button.new + button.add(Clapton::Text.new(@state.done ? "✅" : "🟩")) + button.add_action(:click, :TaskListState, :toggle_done) + + text_field = Clapton::TextField.new(@state, :title) + text_field.add_action(:input, :TaskListState, :update_title) + + datetime_field = Clapton::DateTimeField.new(@state, :due) + datetime_field.add_action(:input, :TaskListState, :update_due) + + @root.add(button).add(text_field).add(datetime_field) + @root.render + end +end + +``` + +```ruby +# app/states/task_list_state.rb +class TaskListState < Clapton::State + attribute :tasks + + def add_task(params) + task = Task.create(title: "New Task", due: Date.today, done: false) + self.tasks << { id: task.id, title: task.title, due: task.due, done: task.done } + end + + def toggle_done(params) + task = Task.find(params[:id]) + task.update(done: !params[:done]) + self.tasks.find { |t| t[:id] == params[:id] }[:done] = task.done + end + + def update_title(params) + task = Task.find(params[:id]) + task.update(title: params[:title]) + self.tasks.find { |t| t[:id] == params[:id] }[:title] = task.title + end + + def update_due(params) + task = Task.find(params[:id]) + task.update(due: params[:due]) + self.tasks.find { |t| t[:id] == params[:id] }[:due] = task.due + end +end +``` + +```ruby +# app/states/task_item_state.rb +class TaskItemState < Clapton::State + attribute :id + attribute :title + attribute :due + attribute :done +end +``` + +```ruby +# app/controllers/tasks_controller.rb +class TasksController < ApplicationController + def index + @tasks = Task.all + @components = [ + [:TaskListComponent, { tasks: @tasks.map { |task| { id: task.id, title: task.title, due: task.due, done: task.done } } }] + ] + end +end +``` + +```html +# app/views/layouts/application.html.erb +<%= clapton_javascript_tag %> +``` + +```html +# app/views/tasks/index.html.erb +<%= clapton_tag %> +``` + +Make sure to include the necessary route in your `config/routes.rb`: + +```ruby +mount Clapton::Engine => "/clapton" +``` + +![TODO APP DEMO](./docs/todo-app-demo.gif) + +### Preset Components + +```ruby +block_quote = Clapton::BlockQuote.new +block_quote.add(Clapton::Text.new("Hello")) + +box = Clapton::Box.new +box.add(Clapton::Text.new("Hello")) + +button = Clapton::Button.new +button.add(Clapton::Text.new("Click me")) +button.add_action(:click, :TaskListState, :add_task) + +checkbox = Clapton::Checkbox.new(:ExampleState, :example_attribute, { id: "example-checkbox" }) +checkbox.add_action(:change, :ExampleState, :update_example_attribute) + +code = Clapton::Code.new +code.add(Clapton::Text.new("Hello")) + +datetime_field = Clapton::DateTimeField.new(:ExampleState, :example_attribute, { id: "example-datetime-field" }) + +element = Clapton::Element.new("div", { id: "example-element" }) +element.add(Clapton::Text.new("Hello")) + +emphasis = Clapton::Emphasis.new +emphasis.add(Clapton::Text.new("Hello")) + +form = Clapton::Form.new +form.add(Clapton::Text.new("Hello")) + +heading = Clapton::Heading.new(1) +heading.add(Clapton::Text.new("Hello")) + +image = Clapton::Image.new("https://example.com/image.png", "Example Image") + +link = Clapton::Link.new("https://example.com") +link.add(Clapton::Text.new("Example Link")) + +list = Clapton::List.new +(1..3).each do + item = Clapton::ListItem.new + item.add(Clapton::Text.new("Item #{i}")) + list.add(item) +end + +ordered_list = Clapton::OrderedList.new +(1..3).each do + item = Clapton::ListItem.new + item.add(Clapton::Text.new("Item #{i}")) + ordered_list.add(item) +end + +paragraph = Clapton::Paragraph.new +paragraph.add(Clapton::Text.new("Hello")) + +quote = Clapton::Quote.new +quote.add(Clapton::Text.new("Hello")) + +radio_button = Clapton::RadioButton.new(:ExampleState, :example_attribute, { id: "example-radio-button" }) +radio_button.add_action(:change, :ExampleState, :update_example_attribute) + +select = Clapton::Select.new([{ value: "1", text: "One" }, { value: "2", text: "Two" }], :ExampleState, :example_attribute, { id: "example-select" }) +select.add_action(:change, :ExampleState, :update_example_attribute) + +span = Clapton::Span.new +span.add(Clapton::Text.new("Hello")) + +text_area = Clapton::TextArea.new(:ExampleState, :example_attribute, { id: "example-text-area" }) + +text_field = Clapton::TextField.new(:ExampleState, :example_attribute, { id: "example-text-field" }) + +text = Clapton::Text.new("Hello")` +``` + +### Optional + +#### Action Cable + +Clapton uses Action Cable to broadcast state changes to the client. +If you want to identify the user, you can set the `current_user` in the connection. + +```ruby +# app/channels/application_cable/connection.rb +module ApplicationCable + class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + end + + private + + def find_verified_user + if verified_user = User.find_by(id: cookies.signed[:user_id]) + verified_user + else + reject_unauthorized_connection + end + end + end +end +``` + +### Testing + +#### RSpec + +```ruby +# spec/spec_helper.rb + +RSpec.configure do |config| + config.include Clapton::TestHelper::RSpec, type: :component +end +``` + +```ruby +# spec/components/task_list_component_spec.rb + +describe "TaskListComponent", type: :component do + it "renders" do + render_component("TaskListComponent", tasks: [{ id: 1, title: "Task 1", done: false, due: Time.current }]) + # You can use Capybara matchers here + expect(page).to have_selector("input[type='text']") + end +end +``` + +#### Minitest + +```ruby +# test/test_helper.rb +class ActiveSupport::TestCase + include Clapton::TestHelper::Minitest +end +``` + +```ruby +# test/components/task_list_component_test.rb +class TaskListComponentTest < ActiveSupport::TestCase + test "renders" do + render_component("TaskListComponent", tasks: [{ id: 1, title: "Task 1", done: false, due: Time.current }]) + # You can use Capybara matchers here + assert_select "input[type='text']" + end +end +``` + +## Development + +After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/dev` to start the development server. + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/kawakamimoeki/clapton. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/kawakamimoeki/clapton/blob/main/CODE_OF_CONDUCT.md). + +## License + +The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..15bdf10 --- /dev/null +++ b/Rakefile @@ -0,0 +1,13 @@ +require "bundler/setup" + +require "bundler/gem_tasks" + +require "rake/testtask" + +Rake::TestTask.new(:test) do |t| + t.libs << 'test' + t.pattern = 'test/**/*_test.rb' + t.verbose = false +end + +task default: :test diff --git a/app/channels/clapton/clapton_channel.rb b/app/channels/clapton/clapton_channel.rb new file mode 100644 index 0000000..e756d1f --- /dev/null +++ b/app/channels/clapton/clapton_channel.rb @@ -0,0 +1,67 @@ +module Clapton + class ClaptonChannel < ApplicationCable::Channel + def subscribed + stream_from "clapton_channel" + end + + def action(data) + state = data["data"]["state"]["name"].constantize.new(JSON.parse(data["data"]["state"]["attributes"].to_json, symbolize_names: true)) + + if state.respond_to?(data["data"]["state"]["action"]) + state.public_send(data["data"]["state"]["action"], JSON.parse(data["data"]["params"].to_json, symbolize_names: true)) do |options = {}| + ActionCable.server.broadcast("clapton_channel", { + status: "success", + continue: options[:continue], + stream: true, + data: { + component: { + name: data["data"]["component"]["name"], + id: data["data"]["component"]["id"], + }, + state: state.to_h, + } + }) + end + if state.errors.any? + ActionCable.server.broadcast("clapton_channel", { + status: "error", + errors: state.errors, + data: { + component: { + name: data["data"]["component"]["name"], + id: data["data"]["component"]["id"], + }, + state: state.to_h, + } + }) + return + end + + ActionCable.server.broadcast("clapton_channel", { + status: "success", + stream: false, + continue: false, + data: { + component: { + name: data["data"]["component"]["name"], + id: data["data"]["component"]["id"], + }, + state: state.to_h, + } + }) + else + ActionCable.server.broadcast("clapton_channel", { + status: "error", + message: "Invalid action", + data: { + component: { + name: data["data"]["component"]["name"], + id: data["data"]["component"]["id"], + }, + state: state.to_h, + } + }) + end + end + end +end diff --git a/app/helpers/clapton/clapton_helper.rb b/app/helpers/clapton/clapton_helper.rb new file mode 100644 index 0000000..d1b9fd7 --- /dev/null +++ b/app/helpers/clapton/clapton_helper.rb @@ -0,0 +1,21 @@ +module Clapton + module ClaptonHelper + + def clapton_javascript_tag + tag.script(src: "/clapton/index.js", type: "text/javascript") + end + + def clapton_tag + datas = [] + @components.each do |component| + state_class = component[0].to_s.gsub("Component", "State") + if Object.const_defined?(state_class) + datas << { component: component[0].to_s, state: Object.const_get(state_class).new(component[1]).to_h } + else + datas << { component: component[0].to_s, state: {} } + end + end + tag.div(id: "clapton", data: { clapton: datas }) + end + end +end diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000..ee70371 --- /dev/null +++ b/bin/dev @@ -0,0 +1,2 @@ +cd lib/clapton/javascripts/ +npm run dev diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..9ae5c14 --- /dev/null +++ b/bin/setup @@ -0,0 +1,3 @@ +bundle install +cd lib/clapton/javascripts +npm i diff --git a/bin/test b/bin/test new file mode 100755 index 0000000..0fcfc86 --- /dev/null +++ b/bin/test @@ -0,0 +1,6 @@ +bundle exec rake +cd test/dummy +bundle exec rake test +bundle exec rspec +cd ../../lib/clapton/javascripts +npm run test diff --git a/clapton.gemspec b/clapton.gemspec new file mode 100644 index 0000000..257ae17 --- /dev/null +++ b/clapton.gemspec @@ -0,0 +1,28 @@ +require_relative "lib/clapton/version" + +Gem::Specification.new do |spec| + spec.name = "clapton" + spec.version = Clapton::VERSION + spec.authors = ["Moeki Kawakami"] + spec.email = ["moeki.kawakami@icloud.com"] + spec.homepage = "https://github.com/kawakamimoeki/clapton" + spec.summary = "Clapton is a Ruby on Rails gem for building web apps with pure Ruby only (no JavaScript and no HTML templates)." + spec.description = "Clapton is a Ruby on Rails gem for building web apps with pure Ruby only (no JavaScript and no HTML templates)." + spec.license = "MIT" + + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' + # to allow pushing to a single host or delete this section to allow pushing to any host. + spec.metadata["allowed_push_host"] = "https://rubygems.org" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://github.com/kawakamimoeki/clapton" + spec.metadata["changelog_uri"] = "https://github.com/kawakamimoeki/clapton/blob/main/CHANGELOG.md" + + spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] + + spec.add_dependency "rails", "~> 6.1.7", ">= 6.1.7.8" + spec.add_dependency "ruby2js", "~> 5" + spec.add_dependency "listen", "~> 3" + spec.add_dependency "capybara", "~> 3" + spec.add_dependency "execjs", "~>2" +end diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..07c5df1 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,4 @@ +Clapton::Engine.routes.draw do + post '/api/action', to: 'api#index' + mount ActionCable.server => '/cable' +end diff --git a/docs/todo-app-demo.gif b/docs/todo-app-demo.gif new file mode 100644 index 0000000..7f5ea15 Binary files /dev/null and b/docs/todo-app-demo.gif differ diff --git a/lib/clapton.rb b/lib/clapton.rb new file mode 100644 index 0000000..0ad5ac7 --- /dev/null +++ b/lib/clapton.rb @@ -0,0 +1,10 @@ +require "ruby2js" +require "clapton/version" +require "clapton/railtie" +require "clapton/engine" +require "clapton/state" +require "clapton/test_helper/rspec" +require "clapton/test_helper/minitest" + +module Clapton +end diff --git a/lib/clapton/engine.rb b/lib/clapton/engine.rb new file mode 100644 index 0000000..4926fb9 --- /dev/null +++ b/lib/clapton/engine.rb @@ -0,0 +1,49 @@ +require "ruby2js" +require "listen" + +module Clapton + class Engine < ::Rails::Engine + isolate_namespace Clapton + + initializer "clapton.helpers" do + ActiveSupport.on_load(:action_view) do + include ClaptonHelper + end + end + + initializer "clapton.action_cable_helpers" do + ActiveSupport.on_load(:action_cable) do + ActionCable.server.config.logger = Rails.logger + end + + components_path = Rails.root.join("app", "components") + FileUtils.mkdir_p(components_path) unless components_path.exist? + FileUtils.touch(components_path.join(".keep")) + + compile_components + + listener = Listen.to(Rails.root.join("app", "components")) do |modified, added, removed| + compile_components + end + + listener.start + end + + def compile_components + js = File.read(File.join(__dir__, "javascripts", "dist", "components.js")) + js += "\n" + js += File.read(File.join(__dir__, "javascripts", "dist", "client.js")) + js += "\n" + js += "window.components = [];" + js += "\n" + Dir.glob(Rails.root.join("app", "components", "**", "*.rb")).each do |file| + js += Ruby2JS.convert(File.read(file), preset: true) + js += "\n" + js += "window.#{File.basename(file, ".rb").camelize} = #{File.basename(file, ".rb").camelize};" + js += "\n" + end + FileUtils.mkdir_p(Rails.root.join("public", "clapton")) unless Rails.root.join("public", "clapton").exist? + File.write(Rails.root.join("public", "clapton", "index.js"), js) + end + end +end diff --git a/lib/clapton/javascripts/package-lock.json b/lib/clapton/javascripts/package-lock.json new file mode 100644 index 0000000..cb90a3a --- /dev/null +++ b/lib/clapton/javascripts/package-lock.json @@ -0,0 +1,3059 @@ +{ + "name": "clapton", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "clapton", + "license": "ISC", + "dependencies": { + "@rails/actioncable": "^7.2.100", + "import": "^0.0.6", + "morphdom": "^2.7.4" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^28.0.0", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-typescript": "^12.1.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@vitest/browser": "^2.1.2", + "jsdom": "^25.0.1", + "rollup": "^4.24.0", + "tslib": "^2.7.0", + "typescript": "^5.5.4", + "vitest": "^2.1.2" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "dev": true + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", + "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", + "dev": true, + "dependencies": { + "cookie": "^0.5.0" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@bundled-es-modules/tough-cookie/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@inquirer/confirm": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.2.0.tgz", + "integrity": "sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz", + "integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@mswjs/interceptors": { + "version": "0.35.9", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.35.9.tgz", + "integrity": "sha512-SSnyl/4ni/2ViHKkiZb8eajA/eN1DNFaHjhGiLUdZvDz6PKF4COSf/17xqSz64nOo2Ia29SA6B2KNCsyCbVmaQ==", + "dev": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true + }, + "node_modules/@rails/actioncable": { + "version": "7.2.100", + "resolved": "https://registry.npmjs.org/@rails/actioncable/-/actioncable-7.2.100.tgz", + "integrity": "sha512-7xtIENf0Yw59AFDM3+xqxPCZxev3QVAqjPmUzmgsB9eL8S/zTpB0IU9srNc7XknzJI4e09XKNnCaJRx3gfYzXA==" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.0.tgz", + "integrity": "sha512-BJcu+a+Mpq476DMXG+hevgPSl56bkUoi88dKT8t3RyUp8kGuOh+2bU8Gs7zXDlu+fyZggnJ+iOBGrb/O1SorYg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.1.1", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.0.tgz", + "integrity": "sha512-Kzs8KGJofe7cfTRODsnG1jNGxSvU8gVoNNd7Z/QaY25AYwe2LSSUpx/kPxqF38NYkpR8de3m51r9uwJpDlz6dg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0||^3.0.0||^4.0.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + }, + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", + "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/react": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.1.tgz", + "integrity": "sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, + "node_modules/@vitest/browser": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-2.1.2.tgz", + "integrity": "sha512-tqpGfz2sfjFFNuZ2iLZ6EGRVnH8z18O93ZIicbLsxDhiLgRNz84UcjSvX4pbheuddW+BJeNbLGdM3BU8vohbEg==", + "dev": true, + "dependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/user-event": "^14.5.2", + "@vitest/mocker": "2.1.2", + "@vitest/utils": "2.1.2", + "magic-string": "^0.30.11", + "msw": "^2.3.5", + "sirv": "^2.0.4", + "tinyrainbow": "^1.2.0", + "ws": "^8.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "2.1.2", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.2.tgz", + "integrity": "sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.2", + "@vitest/utils": "2.1.2", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.2.tgz", + "integrity": "sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA==", + "dev": true, + "dependencies": { + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/spy": "2.1.2", + "msw": "^2.3.5", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.2.tgz", + "integrity": "sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.2.tgz", + "integrity": "sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.1.2", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.2.tgz", + "integrity": "sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.2", + "magic-string": "^0.30.11", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.2.tgz", + "integrity": "sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.2.tgz", + "integrity": "sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.2", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/fdir": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.0.tgz", + "integrity": "sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/import/-/import-0.0.6.tgz", + "integrity": "sha512-QPhTdjy9J4wUzmWSG7APkSgMFuPGPw+iJTYUblcfc2AfpqaatbwgCldK1HoLYx+v/+lWvab63GWZtNkcnj9JcQ==", + "dependencies": { + "optimist": "0.3.x" + }, + "bin": { + "import": "import" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/morphdom": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.4.tgz", + "integrity": "sha512-ATTbWMgGa+FaMU3FhnFYB6WgulCqwf6opOll4CBzmVDTLvPMmUPrEv8CudmLPK0MESa64+6B89fWOxP3+YIlxQ==" + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/msw": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.4.10.tgz", + "integrity": "sha512-bDQh9b25JK4IKMs5hnamwAkcNZ9RwA4mR/4YcgWkzwHOxj7UICbVJfmChJvY1UCAAMraPpvjHdxjoUDpc3F+Qw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^3.0.0", + "@mswjs/interceptors": "^0.35.8", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.2", + "path-to-regexp": "^6.3.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.9.0", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "dev": true + }, + "node_modules/optimist": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==", + "dependencies": { + "wordwrap": "~0.0.2" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", + "dev": true + }, + "node_modules/tinypool": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.50", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.50.tgz", + "integrity": "sha512-q9GOap6q3KCsLMdOjXhWU5jVZ8/1dIib898JBRLsN+tBhENpBDcAVQbE0epADOjw11FhQQy9AcbqKGBQPUfTQA==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.50" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.50", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.50.tgz", + "integrity": "sha512-na2EcZqmdA2iV9zHV7OHQDxxdciEpxrjbkp+aHmZgnZKHzoElLajP59np5/4+sare9fQBfixgvXKx8ev1d7ytw==", + "dev": true + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, + "node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.2.tgz", + "integrity": "sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.6", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.2.tgz", + "integrity": "sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.2", + "@vitest/mocker": "2.1.2", + "@vitest/pretty-format": "^2.1.2", + "@vitest/runner": "2.1.2", + "@vitest/snapshot": "2.1.2", + "@vitest/spy": "2.1.2", + "@vitest/utils": "2.1.2", + "chai": "^5.1.1", + "debug": "^4.3.6", + "magic-string": "^0.30.11", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.2", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.2", + "@vitest/ui": "2.1.2", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/lib/clapton/javascripts/package.json b/lib/clapton/javascripts/package.json new file mode 100644 index 0000000..6eaa372 --- /dev/null +++ b/lib/clapton/javascripts/package.json @@ -0,0 +1,29 @@ +{ + "name": "clapton", + "scripts": { + "build": "rollup -c", + "test": "vitest", + "dev": "rollup -c -w" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@rollup/plugin-commonjs": "^28.0.0", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-typescript": "^12.1.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@vitest/browser": "^2.1.2", + "jsdom": "^25.0.1", + "rollup": "^4.24.0", + "tslib": "^2.7.0", + "typescript": "^5.5.4", + "vitest": "^2.1.2" + }, + "dependencies": { + "@rails/actioncable": "^7.2.100", + "import": "^0.0.6", + "morphdom": "^2.7.4" + } +} diff --git a/lib/clapton/javascripts/rollup.config.mjs b/lib/clapton/javascripts/rollup.config.mjs new file mode 100644 index 0000000..ff92538 --- /dev/null +++ b/lib/clapton/javascripts/rollup.config.mjs @@ -0,0 +1,36 @@ +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import typescript from '@rollup/plugin-typescript'; + +export default [ + { + input: 'src/components.ts', + output: { + file: 'dist/components.js', + format: 'iife', + name: 'Clapton' + }, + plugins: [ + resolve(), + commonjs(), + typescript({ + tsconfig: './tsconfig.json', + sourceMap: false + }) + ] + }, + { + input: 'src/client.ts', + output: { + file: 'dist/client.js', + }, + plugins: [ + resolve(), + commonjs(), + typescript({ + tsconfig: './tsconfig.json', + sourceMap: false + }) + ] + } +]; diff --git a/lib/clapton/javascripts/src/actions/handle-action.spec.ts b/lib/clapton/javascripts/src/actions/handle-action.spec.ts new file mode 100644 index 0000000..fd74d6d --- /dev/null +++ b/lib/clapton/javascripts/src/actions/handle-action.spec.ts @@ -0,0 +1,46 @@ +import { handleAction } from "./handle-action"; +import { describe, it, expect, vi } from "vitest"; +import { claptonChannel } from "../channel/clapton-channel" + +describe("handleAction", () => { + it("runs the action and calls claptonChannel.perform", async () => { + const componentWrapper = document.createElement("div"); + componentWrapper.innerHTML = `
`; + + const input = document.createElement("input"); + input.setAttribute("data-attribute", "testAttribute"); + input.value = "updated"; + + const component = componentWrapper.firstChild as HTMLElement; + component.appendChild(input); + + document.body.appendChild(component); + + const performSpy = vi.spyOn(claptonChannel, "perform"); + + await handleAction(input, "TestState", "testFunction"); + + expect(component.getAttribute("data-state")).toBe('{"testAttribute":"updated"}'); + expect(performSpy).toHaveBeenCalledWith("action", { + action: "action", + data: { + state: { + name: "TestState", + action: "testFunction", + attributes: { + testAttribute: "updated" + } + }, + component: { + name: "TestComponent", + id: "1", + }, + params: { + testAttribute: "updated" + } + } + }); + + performSpy.mockRestore(); + }); +}); diff --git a/lib/clapton/javascripts/src/actions/handle-action.ts b/lib/clapton/javascripts/src/actions/handle-action.ts new file mode 100644 index 0000000..bfb12bb --- /dev/null +++ b/lib/clapton/javascripts/src/actions/handle-action.ts @@ -0,0 +1,32 @@ +import { claptonChannel } from "../channel/clapton-channel"; + +export const handleAction = async (target: HTMLElement, stateName: string, fn: string) => { + const targetComponent = target.closest(`[data-component="${stateName.replace("State", "Component")}"]`) as HTMLElement; + if (!targetComponent) return; + const component = target.closest(`[data-component]`) as HTMLElement; + const attribute = target.getAttribute("data-attribute"); + if (attribute) { + const state = JSON.parse(component.getAttribute("data-state") || "{}"); + if (target.tagName === "INPUT") { + state[attribute] = (target as HTMLInputElement).value; + component.setAttribute("data-state", JSON.stringify(state)); + } + }; + claptonChannel.perform( + "action", + { + data: { + component: { + name: stateName.replace("State", "Component"), + id: targetComponent.getAttribute("data-id"), + }, + state: { + name: stateName, + action: fn, + attributes: JSON.parse(targetComponent.getAttribute("data-state") || "{}"), + }, + params: JSON.parse(component.getAttribute("data-state") || "{}") + } + } + ); +}; diff --git a/lib/clapton/javascripts/src/actions/initialize-actions.ts b/lib/clapton/javascripts/src/actions/initialize-actions.ts new file mode 100644 index 0000000..41ee9bb --- /dev/null +++ b/lib/clapton/javascripts/src/actions/initialize-actions.ts @@ -0,0 +1,28 @@ +import { splitActionAttribute } from "../html/split-action-attribute"; +import { handleAction } from "./handle-action"; +import { debounce } from "../utils/debounce"; + +export const initializeActions = () => { + const actionElements = document.querySelectorAll("[data-action]"); + actionElements.forEach((element) => initializeActionsForElement(element as HTMLElement)); +}; + +const initializeActionsForElement = (element: HTMLElement) => { + if (element.getAttribute("data-set-event-handler")) return; + const actions = element.getAttribute("data-action")?.split(" ") || []; + actions.forEach(action => { + const { eventType, componentName, stateName, fnName, bounceTime } = splitActionAttribute(action); + if (!eventType || !componentName || !fnName) return; + + if (bounceTime > 0) { + element.addEventListener(eventType, debounce((event) => + handleAction(event.target as HTMLElement, stateName, fnName), bounceTime) + ); + } else { + element.addEventListener(eventType, (event) => + handleAction(event.target as HTMLElement, stateName, fnName) + ); + } + element.setAttribute("data-set-event-handler", "true"); + }); +}; diff --git a/lib/clapton/javascripts/src/channel/clapton-channel.js b/lib/clapton/javascripts/src/channel/clapton-channel.js new file mode 100644 index 0000000..cad9821 --- /dev/null +++ b/lib/clapton/javascripts/src/channel/clapton-channel.js @@ -0,0 +1,25 @@ +import morphdom from "morphdom" +import { createConsumer } from "@rails/actioncable" +import { initializeActions } from "../actions/initialize-actions.ts" + +const consumer = createConsumer() + +export const claptonChannel = consumer.subscriptions.create("Clapton::ClaptonChannel", { + connected() {}, + + disconnected() {}, + + received(response) { + const { data, errors } = response; + const component = document.querySelector(`[data-id="${data.component.id}"]`) + const instance = new window[data.component.name](data.state, data.component.id, errors); + morphdom(component, instance.render, { + onBeforeElUpdated: (_fromEl, toEl) => { + toEl.setAttribute("data-set-event-handler", "true"); + return true; + } + }); + initializeInputs(); + initializeActions(); + } +}) diff --git a/lib/clapton/javascripts/src/client.ts b/lib/clapton/javascripts/src/client.ts new file mode 100644 index 0000000..0f16a7d --- /dev/null +++ b/lib/clapton/javascripts/src/client.ts @@ -0,0 +1,37 @@ +import { splitActionAttribute } from "./html/split-action-attribute" +import { updateComponent } from "./dom/update-component" +import { handleAction } from "./actions/handle-action" +import { initializeActions } from "actions/initialize-actions"; +import { initializeInputs } from "inputs/initialize-inputs"; + +interface ComponentDefinition { + component: new (state: any) => ComponentInstance; + state: any; + id: string; +} + +interface ComponentInstance { + render: string; + [key: string]: any; +} + +const initializeComponents = () => { + const components = document.querySelector("#clapton")?.getAttribute("data-clapton") || "[]"; + JSON.parse(components).forEach(createAndAppendComponent); +}; + +const createAndAppendComponent = (component: ComponentDefinition) => { + const componentDom = document.createElement('div'); + const instance = new (window[component.component as any] as any)(component.state); + componentDom.innerHTML = instance.render; + const firstChild = componentDom.firstChild as HTMLElement; + if (firstChild) { + document.querySelector("#clapton")?.appendChild(firstChild); + } +}; + +document.addEventListener("DOMContentLoaded", () => { + initializeComponents(); + initializeActions(); + initializeInputs(); +}); diff --git a/lib/clapton/javascripts/src/components.ts b/lib/clapton/javascripts/src/components.ts new file mode 100644 index 0000000..d8113a5 --- /dev/null +++ b/lib/clapton/javascripts/src/components.ts @@ -0,0 +1,9 @@ +import { Box } from "./components/box" +import { Component } from "./components/component" +import { Text } from "./components/text" +import { TextField } from "./components/text-field" +import { Link } from "./components/link" +import { Button } from "./components/button" +import { DateTimeField } from "./components/datetime-field" + +export { Box, Component, Text, TextField, Link, Button, DateTimeField }; diff --git a/lib/clapton/javascripts/src/components/block-quote.spec.ts b/lib/clapton/javascripts/src/components/block-quote.spec.ts new file mode 100644 index 0000000..625f2b4 --- /dev/null +++ b/lib/clapton/javascripts/src/components/block-quote.spec.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest" +import { BlockQuote } from "./block-quote" +import { Text } from "./text" + +describe("BlockQuote", () => { + it("returns empty string if no params", () => { + expect(new BlockQuote().render).toBe("
") + }) + + it("returns attributes and data attributes", () => { + expect(new BlockQuote({ id: "1", "data-foo": "bar" }).render).toBe(`
`) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new BlockQuote({ id: "1", data: { foo: "bar" } }).render).toBe(`
`) + expect(new BlockQuote({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(`
`) + expect(new BlockQuote({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(`
`) + }) + + it("adds children", () => { + const text = new Text("Hello") + const blockQuote = new BlockQuote() + blockQuote.add(text) + expect(blockQuote.render).toBe(`
Hello
`) + }) +}) diff --git a/lib/clapton/javascripts/src/components/block-quote.ts b/lib/clapton/javascripts/src/components/block-quote.ts new file mode 100644 index 0000000..234e725 --- /dev/null +++ b/lib/clapton/javascripts/src/components/block-quote.ts @@ -0,0 +1,20 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class BlockQuote { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + } + + get render(): string { + return `
${this.children.map(child => child.render).join("")}
`; + } + + add(child: any): BlockQuote { + this.children.push(child); + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/box.spec.ts b/lib/clapton/javascripts/src/components/box.spec.ts new file mode 100644 index 0000000..db84416 --- /dev/null +++ b/lib/clapton/javascripts/src/components/box.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest" +import { Box } from "./box" +import { Text } from "./text" + +describe("Box", () => { + it("returns empty string if no params", () => { + expect(new Box().render).toBe("
") + }) + + it("returns attributes and data attributes", () => { + expect(new Box({ id: "1", "data-foo": "bar" }).render).toBe(`
`) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Box({ id: "1", data: { foo: "bar" } }).render).toBe(`
`) + expect(new Box({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(`
`) + expect(new Box({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(`
`) + }) + + it("adds children", () => { + expect(new Box().add(new Text("Hello, world!")).render).toBe(`
Hello, world!
`) + }) +}) diff --git a/lib/clapton/javascripts/src/components/box.ts b/lib/clapton/javascripts/src/components/box.ts new file mode 100644 index 0000000..76f37a5 --- /dev/null +++ b/lib/clapton/javascripts/src/components/box.ts @@ -0,0 +1,20 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Box { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + } + + add(child: any): this { + this.children.push(child); + return this; + } + + get render(): string { + return `
${this.children.map(child => child.render).join("")}
`; + } +} diff --git a/lib/clapton/javascripts/src/components/button.spec.ts b/lib/clapton/javascripts/src/components/button.spec.ts new file mode 100644 index 0000000..ce2d59f --- /dev/null +++ b/lib/clapton/javascripts/src/components/button.spec.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from "vitest" +import { Button } from "./button" + +describe("Button", () => { + it("returns empty string if no params", () => { + expect(new Button().render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new Button({ id: "1", "data-foo": "bar" }).render).toBe(``) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Button({ id: "1", data: { foo: "bar" } }).render).toBe(``) + expect(new Button({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(``) + expect(new Button({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(``) + }) +}) diff --git a/lib/clapton/javascripts/src/components/button.ts b/lib/clapton/javascripts/src/components/button.ts new file mode 100644 index 0000000..c1d340e --- /dev/null +++ b/lib/clapton/javascripts/src/components/button.ts @@ -0,0 +1,25 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Button { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.attributes = attributes; + this.children = []; + } + + add(child: any): Button { + this.children.push(child); + return this; + } + + get render(): string { + return ``; + } + + add_action(event: string, klass: string, fn: string, options: Record = {}): Button { + this.attributes["data-action"] = `${this.attributes["data-action"] || ""} ${event}->${klass}#${fn}@${options.debounce || 0}`; + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/checkbox.spec.ts b/lib/clapton/javascripts/src/components/checkbox.spec.ts new file mode 100644 index 0000000..c413c88 --- /dev/null +++ b/lib/clapton/javascripts/src/components/checkbox.spec.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest" +import { Checkbox } from "./checkbox" + +describe("Checkbox", () => { + it("returns empty string if no params", () => { + expect(new Checkbox({}, "foo").render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new Checkbox({ foo: "bar" }, "foo", { id: "1", "data-foo": "bar" }).render).toBe(``) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Checkbox({ foo: "bar" }, "foo", { id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(``) + }) +}) diff --git a/lib/clapton/javascripts/src/components/checkbox.ts b/lib/clapton/javascripts/src/components/checkbox.ts new file mode 100644 index 0000000..6daf17f --- /dev/null +++ b/lib/clapton/javascripts/src/components/checkbox.ts @@ -0,0 +1,23 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Checkbox { + state: any; + attribute: string; + attributes: Record; + + constructor(state: any, attribute: string, attributes: Record = {}) { + this.state = state; + this.attributes = attributes; + this.attribute = attribute; + this.attributes["data-attribute"] = attribute; + } + + get render(): string { + return ``; + } + + add_action(event: string, klass: string, fn: string, options: { debounce?: number } = {}): Checkbox { + this.attributes["data-action"] = `${this.attributes["data-action"] || ""} ${event}->${klass}#${fn}@${options.debounce || 0}`; + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/code.spec.ts b/lib/clapton/javascripts/src/components/code.spec.ts new file mode 100644 index 0000000..e335399 --- /dev/null +++ b/lib/clapton/javascripts/src/components/code.spec.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest" +import { Code } from "./code" +import { Text } from "./text" + +describe("Code", () => { + it("returns empty string if no params", () => { + expect(new Code().render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new Code({ id: "1", "data-foo": "bar" }).render).toBe(``) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Code({ id: "1", data: { foo: "bar" } }).render).toBe(``) + expect(new Code({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(``) + expect(new Code({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(``) + }) + + it("adds children", () => { + const text = new Text("Hello") + const code = new Code() + code.add(text) + expect(code.render).toBe(`Hello`) + }) +}) diff --git a/lib/clapton/javascripts/src/components/code.ts b/lib/clapton/javascripts/src/components/code.ts new file mode 100644 index 0000000..47022dd --- /dev/null +++ b/lib/clapton/javascripts/src/components/code.ts @@ -0,0 +1,20 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Code { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + } + + get render(): string { + return `${this.children.map(child => child.render).join("")}`; + } + + add(child: any): Code { + this.children.push(child); + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/component.spec.ts b/lib/clapton/javascripts/src/components/component.spec.ts new file mode 100644 index 0000000..1acf77a --- /dev/null +++ b/lib/clapton/javascripts/src/components/component.spec.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest" +import { Component } from "./component" +import { Text } from "./text" + +class TestComponent extends Component { + get render() { + this._root.add(new Text("Hello, world!")) + return this._root.render + } +} + +describe("Component", () => { + it("returns empty string if no params", () => { + expect(new TestComponent().render).toMatch(/
Hello, world!<\/div>/) + }) +}) diff --git a/lib/clapton/javascripts/src/components/component.ts b/lib/clapton/javascripts/src/components/component.ts new file mode 100644 index 0000000..0b4c63b --- /dev/null +++ b/lib/clapton/javascripts/src/components/component.ts @@ -0,0 +1,19 @@ +import { Box } from "./box"; + +export class Component { + id: string; + _state: any; + _errors: any[]; + _root: Box; + + constructor(state: any = {}, id: string = Math.random().toString(36).substring(2, 10), errors: any[] = []) { + this._state = state; + this.id = id; + this._errors = errors; + this._root = new Box({ data: { component: this.constructor.name, state: JSON.stringify(this._state), id: this.id, errors: this._errors } }); + } + + get render(): string { + return this._root.render; + } +} diff --git a/lib/clapton/javascripts/src/components/datetime-field.spec.ts b/lib/clapton/javascripts/src/components/datetime-field.spec.ts new file mode 100644 index 0000000..af3285b --- /dev/null +++ b/lib/clapton/javascripts/src/components/datetime-field.spec.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest" +import { DateTimeField } from "./datetime-field" + +describe("DateTimeField", () => { + it("returns empty string if no params", () => { + expect(new DateTimeField({}, "foo").render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new DateTimeField({ foo: new Date("2024-10-12T12:00") }, "foo", { id: "1", "data-foo": "bar" }).render).toMatch(//) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new DateTimeField({ foo: new Date("2024-10-12T12:00") }, "foo", { id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toMatch(//) + }) +}) diff --git a/lib/clapton/javascripts/src/components/datetime-field.ts b/lib/clapton/javascripts/src/components/datetime-field.ts new file mode 100644 index 0000000..469e5e9 --- /dev/null +++ b/lib/clapton/javascripts/src/components/datetime-field.ts @@ -0,0 +1,49 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class DateTimeField { + state: any; + attribute: string; + attributes: Record = {}; + + constructor(state: any, attribute: string, attributes: Record = {}) { + this.state = state; + this.attribute = attribute; + this.attributes = attributes; + this.attributes["data-attribute"] = attribute; + } + + get render(): string { + const value = this.state[this.attribute] ? this.datetime_local_value(this.state[this.attribute]) : ""; + return ``; + } + + add_action(event: string, klass: string, fn: string, options: { debounce?: number } = {}): DateTimeField { + this.attributes["data-action"] = `${this.attributes["data-action"] || ""} ${event}->${klass}#${fn}@${options.debounce || 0}`; + return this; + } + + datetime_local_value(value: string): string { + if (!value) { + return ""; + } + const date = new Date(value); + date.setMinutes(date.getMinutes() - date.getTimezoneOffset()); + let month: string | number = date.getMonth() + 1; + let day: string | number = date.getDate(); + let hours: string | number = date.getHours(); + let minutes: string | number = date.getMinutes(); + if (month < 10) { + month = `0${month}`; + } + if (day < 10) { + day = `0${day}`; + } + if (hours < 10) { + hours = `0${hours}`; + } + if (minutes < 10) { + minutes = `0${minutes}`; + } + return `${date.getFullYear()}-${month}-${day}T${hours}:${minutes}`; + } +} diff --git a/lib/clapton/javascripts/src/components/element.spec.ts b/lib/clapton/javascripts/src/components/element.spec.ts new file mode 100644 index 0000000..96431ff --- /dev/null +++ b/lib/clapton/javascripts/src/components/element.spec.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from "vitest" +import { Element } from "./element" +import { BlockQuote } from "./block-quote" +import { Text } from "./text" + +describe("Element", () => { + it("returns empty string if no params", () => { + expect(new Element("blockquote").render).toBe("
") + }) + + it("returns attributes and data attributes", () => { + expect(new Element("blockquote", { id: "1", "data-foo": "bar" }).render).toBe(`
`) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Element("blockquote", { id: "1", data: { foo: "bar" } }).render).toBe(`
`) + expect(new Element("blockquote", { id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(`
`) + expect(new Element("blockquote", { id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(`
`) + }) + + it("adds children", () => { + const text = new Text("Hello") + const blockQuote = new Element("blockquote") + blockQuote.add(text) + expect(blockQuote.render).toBe(`
Hello
`) + }) +}) diff --git a/lib/clapton/javascripts/src/components/element.ts b/lib/clapton/javascripts/src/components/element.ts new file mode 100644 index 0000000..bd92e5e --- /dev/null +++ b/lib/clapton/javascripts/src/components/element.ts @@ -0,0 +1,22 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Element { + attributes: Record; + children: any[]; + type: string; + + constructor(type: string, attributes: Record = {}) { + this.children = []; + this.type = type; + this.attributes = attributes; + } + + get render(): string { + return `<${this.type} ${htmlAttributes(this.attributes)}>${this.children.map(child => child.render).join("")}`; + } + + add(child: any): Element { + this.children.push(child); + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/emphasis.spec.ts b/lib/clapton/javascripts/src/components/emphasis.spec.ts new file mode 100644 index 0000000..5d15db0 --- /dev/null +++ b/lib/clapton/javascripts/src/components/emphasis.spec.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest" +import { Emphasis } from "./emphasis" +import { Text } from "./text" + +describe("Emphasis", () => { + it("returns empty string if no params", () => { + expect(new Emphasis().render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new Emphasis({ id: "1", "data-foo": "bar" }).render).toBe(``) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Emphasis({ id: "1", data: { foo: "bar" } }).render).toBe(``) + expect(new Emphasis({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(``) + expect(new Emphasis({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(``) + }) + + it("adds children", () => { + const text = new Text("Hello") + const emphasis = new Emphasis() + emphasis.add(text) + expect(emphasis.render).toBe(`Hello`) + }) +}) diff --git a/lib/clapton/javascripts/src/components/emphasis.ts b/lib/clapton/javascripts/src/components/emphasis.ts new file mode 100644 index 0000000..717b648 --- /dev/null +++ b/lib/clapton/javascripts/src/components/emphasis.ts @@ -0,0 +1,20 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Emphasis { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + } + + get render(): string { + return `${this.children.map(child => child.render).join("")}`; + } + + add(child: any): Emphasis { + this.children.push(child); + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/form.spec.ts b/lib/clapton/javascripts/src/components/form.spec.ts new file mode 100644 index 0000000..bded825 --- /dev/null +++ b/lib/clapton/javascripts/src/components/form.spec.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest" +import { Form } from "./form" +import { Text } from "./text" + +describe("Form", () => { + it("returns empty string if no params", () => { + expect(new Form().render).toBe("
") + }) + + it("returns attributes and data attributes", () => { + expect(new Form({ id: "1", "data-foo": "bar" }).render).toBe(`
`) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Form({ id: "1", data: { foo: "bar" } }).render).toBe(`
`) + expect(new Form({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(`
`) + expect(new Form({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(`
`) + }) + + it("adds children", () => { + const text = new Text("Hello") + const form = new Form() + form.add(text) + expect(form.render).toBe(`
Hello
`) + }) +}) diff --git a/lib/clapton/javascripts/src/components/form.ts b/lib/clapton/javascripts/src/components/form.ts new file mode 100644 index 0000000..7593e0c --- /dev/null +++ b/lib/clapton/javascripts/src/components/form.ts @@ -0,0 +1,20 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Form { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + } + + get render(): string { + return `
${this.children.map(child => child.render).join("")}
`; + } + + add(child: any): Form { + this.children.push(child); + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/heading.spec.ts b/lib/clapton/javascripts/src/components/heading.spec.ts new file mode 100644 index 0000000..64e224e --- /dev/null +++ b/lib/clapton/javascripts/src/components/heading.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest" +import { Heading } from "./heading" +import { Text } from "./text" + +describe("Heading", () => { + it("returns empty string if no params", () => { + expect(new Heading(1).render).toBe("

") + }) + + it("returns attributes and data attributes", () => { + expect(new Heading(1, { id: "1", "data-foo": "bar" }).render).toBe(`

`) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Heading(1, { id: "1", data: { foo: "bar" } }).render).toBe(`

`) + expect(new Heading(1, { id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(`

`) + expect(new Heading(1, { id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(`

`) + }) + + it("adds children", () => { + expect(new Heading(1).add(new Text("Hello")).render).toBe(`

Hello

`) + }) +}) diff --git a/lib/clapton/javascripts/src/components/heading.ts b/lib/clapton/javascripts/src/components/heading.ts new file mode 100644 index 0000000..3323626 --- /dev/null +++ b/lib/clapton/javascripts/src/components/heading.ts @@ -0,0 +1,21 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Heading { + attributes: Record; + children: any[]; + level: number; + constructor(level: number, attributes: Record = {}) { + this.children = []; + this.level = level; + this.attributes = attributes; + } + + get render(): string { + return `${this.children.map(child => child.render).join("")}`; + } + + add(child: any): Heading { + this.children.push(child); + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/image.spec.ts b/lib/clapton/javascripts/src/components/image.spec.ts new file mode 100644 index 0000000..660e8aa --- /dev/null +++ b/lib/clapton/javascripts/src/components/image.spec.ts @@ -0,0 +1,12 @@ +import { describe, it, expect } from "vitest" +import { Image } from "./image" + +describe("Image", () => { + it("returns empty string if no params", () => { + expect(new Image("https://example.com/image.png", "").render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new Image("https://example.com/image.png", "test", { id: "1", "data-foo": "bar" }).render).toBe(`test`) + }) +}) diff --git a/lib/clapton/javascripts/src/components/image.ts b/lib/clapton/javascripts/src/components/image.ts new file mode 100644 index 0000000..e15c404 --- /dev/null +++ b/lib/clapton/javascripts/src/components/image.ts @@ -0,0 +1,19 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Image { + attributes: Record; + children: any[]; + src: string; + alt: string; + + constructor(src: string, alt: string, attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + this.src = src; + this.alt = alt; + } + + get render(): string { + return `${this.alt}`; + } +} diff --git a/lib/clapton/javascripts/src/components/link.spec.ts b/lib/clapton/javascripts/src/components/link.spec.ts new file mode 100644 index 0000000..ecddf2d --- /dev/null +++ b/lib/clapton/javascripts/src/components/link.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest" +import { Link } from "./link" +import { Text } from "./text" + +describe("Link", () => { + it("returns empty string if no params", () => { + expect(new Link("").render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new Link("#").add(new Text("")).render).toBe(``) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Link("#", { id: "1", data: { foo: "bar" } }).render).toBe(``) + expect(new Link("#", { id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(``) + expect(new Link("#", { id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(``) + }) + + it("adds children", () => { + expect(new Link("#").add(new Text("Hello")).render).toBe(`Hello`) + }) +}) diff --git a/lib/clapton/javascripts/src/components/link.ts b/lib/clapton/javascripts/src/components/link.ts new file mode 100644 index 0000000..8fc03aa --- /dev/null +++ b/lib/clapton/javascripts/src/components/link.ts @@ -0,0 +1,21 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Link { + attributes: Record; + children: any[]; + href: string; + constructor(href: string, attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + this.href = href; + } + + get render(): string { + return `${this.children.map(child => child.render).join("")}`; + } + + add(child: any): Link { + this.children.push(child); + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/list-item.spec.ts b/lib/clapton/javascripts/src/components/list-item.spec.ts new file mode 100644 index 0000000..cf497bc --- /dev/null +++ b/lib/clapton/javascripts/src/components/list-item.spec.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from "vitest" +import { ListItem } from "./list-item" +import { Text } from "./text" + +describe("ListItem", () => { + it("returns empty string if no params", () => { + expect(new ListItem().render).toBe("
  • ") + }) + + it("returns attributes and data attributes", () => { + expect(new ListItem({ id: "1", "data-foo": "bar" }).render).toBe(`
  • `) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new ListItem({ id: "1", data: { foo: "bar" } }).render).toBe(`
  • `) + expect(new ListItem({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(`
  • `) + expect(new ListItem({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(`
  • `) + }) + + it("adds children", () => { + const text = new Text("Hello, world!") + expect(new ListItem().add(text).render).toBe(`
  • Hello, world!
  • `) + }) +}) diff --git a/lib/clapton/javascripts/src/components/list-item.ts b/lib/clapton/javascripts/src/components/list-item.ts new file mode 100644 index 0000000..1070bdb --- /dev/null +++ b/lib/clapton/javascripts/src/components/list-item.ts @@ -0,0 +1,20 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class ListItem { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + } + + add(child: any): this { + this.children.push(child); + return this; + } + + get render(): string { + return `
  • ${this.children.map(child => child.render).join("")}
  • `; + } +} diff --git a/lib/clapton/javascripts/src/components/list.spec.ts b/lib/clapton/javascripts/src/components/list.spec.ts new file mode 100644 index 0000000..b3f0b48 --- /dev/null +++ b/lib/clapton/javascripts/src/components/list.spec.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest" +import { List } from "./list" +import { ListItem } from "./list-item" +import { Text } from "./text" + +describe("List", () => { + it("returns empty string if no params", () => { + expect(new List().render).toBe("
      ") + }) + + it("returns attributes and data attributes", () => { + expect(new List({ id: "1", "data-foo": "bar" }).render).toBe(`
        `) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new List({ id: "1", data: { foo: "bar" } }).render).toBe(`
          `) + expect(new List({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(`
            `) + expect(new List({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(`
              `) + }) + + it("adds children", () => { + const listItem = new ListItem() + listItem.add(new Text("Hello, world!")) + expect(new List().add(listItem).render).toBe(`
              • Hello, world!
              `) + }) +}) diff --git a/lib/clapton/javascripts/src/components/list.ts b/lib/clapton/javascripts/src/components/list.ts new file mode 100644 index 0000000..2891507 --- /dev/null +++ b/lib/clapton/javascripts/src/components/list.ts @@ -0,0 +1,20 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class List { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + } + + add(child: any): this { + this.children.push(child); + return this; + } + + get render(): string { + return `
                ${this.children.map(child => child.render).join("")}
              `; + } +} diff --git a/lib/clapton/javascripts/src/components/ordered-list.spec.ts b/lib/clapton/javascripts/src/components/ordered-list.spec.ts new file mode 100644 index 0000000..2024257 --- /dev/null +++ b/lib/clapton/javascripts/src/components/ordered-list.spec.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest" +import { OrderedList } from "./ordered-list" +import { ListItem } from "./list-item" +import { Text } from "./text" + +describe("OrderedList", () => { + it("returns empty string if no params", () => { + expect(new OrderedList().render).toBe("
                ") + }) + + it("returns attributes and data attributes", () => { + expect(new OrderedList({ id: "1", "data-foo": "bar" }).render).toBe(`
                  `) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new OrderedList({ id: "1", data: { foo: "bar" } }).render).toBe(`
                    `) + expect(new OrderedList({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(`
                      `) + expect(new OrderedList({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(`
                        `) + }) + + it("adds children", () => { + const listItem = new ListItem() + listItem.add(new Text("Hello, world!")) + expect(new OrderedList().add(listItem).render).toBe(`
                        1. Hello, world!
                        `) + }) +}) diff --git a/lib/clapton/javascripts/src/components/ordered-list.ts b/lib/clapton/javascripts/src/components/ordered-list.ts new file mode 100644 index 0000000..ccdf9de --- /dev/null +++ b/lib/clapton/javascripts/src/components/ordered-list.ts @@ -0,0 +1,20 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class OrderedList { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + } + + add(child: any): this { + this.children.push(child); + return this; + } + + get render(): string { + return `
                          ${this.children.map(child => child.render).join("")}
                        `; + } +} diff --git a/lib/clapton/javascripts/src/components/paragraph.spec.ts b/lib/clapton/javascripts/src/components/paragraph.spec.ts new file mode 100644 index 0000000..de810c0 --- /dev/null +++ b/lib/clapton/javascripts/src/components/paragraph.spec.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest" +import { Paragraph } from "./paragraph" +import { Text } from "./text" + +describe("Paragraph", () => { + it("returns empty string if no params", () => { + expect(new Paragraph().render).toBe("

                        ") + }) + + it("returns attributes and data attributes", () => { + expect(new Paragraph({ id: "1", "data-foo": "bar" }).render).toBe(`

                        `) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Paragraph({ id: "1", data: { foo: "bar" } }).render).toBe(`

                        `) + expect(new Paragraph({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(`

                        `) + expect(new Paragraph({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(`

                        `) + }) + + it("adds children", () => { + const text = new Text("Hello") + const paragraph = new Paragraph() + paragraph.add(text) + expect(paragraph.render).toBe(`

                        Hello

                        `) + }) +}) diff --git a/lib/clapton/javascripts/src/components/paragraph.ts b/lib/clapton/javascripts/src/components/paragraph.ts new file mode 100644 index 0000000..a3db939 --- /dev/null +++ b/lib/clapton/javascripts/src/components/paragraph.ts @@ -0,0 +1,20 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Paragraph { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + } + + get render(): string { + return `

                        ${this.children.map(child => child.render).join("")}

                        `; + } + + add(child: any): Paragraph { + this.children.push(child); + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/quote.spec.ts b/lib/clapton/javascripts/src/components/quote.spec.ts new file mode 100644 index 0000000..c6c9efc --- /dev/null +++ b/lib/clapton/javascripts/src/components/quote.spec.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest" +import { Text } from "./text" +import { Quote } from "./quote" + +describe("Quote", () => { + it("returns empty string if no params", () => { + expect(new Quote().render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new Quote({ id: "1", "data-foo": "bar" }).render).toBe(``) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Quote({ id: "1", data: { foo: "bar" } }).render).toBe(``) + expect(new Quote({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(``) + expect(new Quote({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(``) + }) + + it("adds children", () => { + const text = new Text("Hello") + const quote = new Quote() + quote.add(text) + expect(quote.render).toBe(`Hello`) + }) +}) diff --git a/lib/clapton/javascripts/src/components/quote.ts b/lib/clapton/javascripts/src/components/quote.ts new file mode 100644 index 0000000..3928ac0 --- /dev/null +++ b/lib/clapton/javascripts/src/components/quote.ts @@ -0,0 +1,20 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Quote { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + } + + get render(): string { + return `${this.children.map(child => child.render).join("")}`; + } + + add(child: any): Quote { + this.children.push(child); + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/radio-button.spec.ts b/lib/clapton/javascripts/src/components/radio-button.spec.ts new file mode 100644 index 0000000..ddc6d91 --- /dev/null +++ b/lib/clapton/javascripts/src/components/radio-button.spec.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest" +import { RadioButton } from "./radio-button" + +describe("RadioButton", () => { + it("returns empty string if no params", () => { + expect(new RadioButton({}, "foo").render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new RadioButton({ foo: "bar" }, "foo", { id: "1", "data-foo": "bar" }).render).toBe(``) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new RadioButton({ foo: "bar" }, "foo", { id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(``) + }) +}) diff --git a/lib/clapton/javascripts/src/components/radio-button.ts b/lib/clapton/javascripts/src/components/radio-button.ts new file mode 100644 index 0000000..689bedb --- /dev/null +++ b/lib/clapton/javascripts/src/components/radio-button.ts @@ -0,0 +1,23 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class RadioButton { + state: any; + attribute: string; + attributes: Record; + + constructor(state: any, attribute: string, attributes: Record = {}) { + this.state = state; + this.attributes = attributes; + this.attribute = attribute; + this.attributes["data-attribute"] = attribute; + } + + get render(): string { + return ``; + } + + add_action(event: string, klass: string, fn: string, options: { debounce?: number } = {}): RadioButton { + this.attributes["data-action"] = `${this.attributes["data-action"] || ""} ${event}->${klass}#${fn}@${options.debounce || 0}`; + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/select.spec.ts b/lib/clapton/javascripts/src/components/select.spec.ts new file mode 100644 index 0000000..105bb88 --- /dev/null +++ b/lib/clapton/javascripts/src/components/select.spec.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from "vitest" +import { Select } from "./select" + +describe("Select", () => { + it("returns empty string if no params", () => { + expect(new Select([], {}, "foo").render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new Select([], {}, "foo", { id: "1", "data-foo": "bar" }).render).toBe(``) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Select([], {}, "foo", { id: "1", data: { foo: "bar" } }).render).toBe(``) + expect(new Select([], {}, "foo", { id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(``) + expect(new Select([], {}, "foo", { id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(``) + }) + + it("returns options", () => { + expect(new Select([{ value: "1", text: "One" }], { foo: "1" }, "foo").render).toBe(``) + }) +}) diff --git a/lib/clapton/javascripts/src/components/select.ts b/lib/clapton/javascripts/src/components/select.ts new file mode 100644 index 0000000..e9b6551 --- /dev/null +++ b/lib/clapton/javascripts/src/components/select.ts @@ -0,0 +1,26 @@ +import { htmlAttributes } from "../html/html-attributes"; + +type SelectOption = { + value: string; + text: string; +}; + +export class Select { + attributes: Record; + children: any[]; + options: SelectOption[]; + state: any; + attribute: string; + + constructor(options: SelectOption[] = [], state: any, attribute: string, attributes: Record = {}) { + this.children = []; + this.options = options; + this.state = state; + this.attribute = attribute; + this.attributes = attributes; + } + + get render(): string { + return ``; + } +} diff --git a/lib/clapton/javascripts/src/components/span.spec.ts b/lib/clapton/javascripts/src/components/span.spec.ts new file mode 100644 index 0000000..3f133be --- /dev/null +++ b/lib/clapton/javascripts/src/components/span.spec.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from "vitest" +import { Text } from "./text" +import { Span } from "./span" + +describe("Span", () => { + it("returns empty string if no params", () => { + expect(new Span().render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new Span({ id: "1", "data-foo": "bar" }).render).toBe(``) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new Span({ id: "1", data: { foo: "bar" } }).render).toBe(``) + expect(new Span({ id: "1", data: { foo: "bar", baz: "qux" } }).render).toBe(``) + expect(new Span({ id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(``) + }) + + it("adds children", () => { + const text = new Text("Hello") + const span = new Span() + span.add(text) + expect(span.render).toBe(`Hello`) + }) +}) diff --git a/lib/clapton/javascripts/src/components/span.ts b/lib/clapton/javascripts/src/components/span.ts new file mode 100644 index 0000000..b561440 --- /dev/null +++ b/lib/clapton/javascripts/src/components/span.ts @@ -0,0 +1,20 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class Span { + attributes: Record; + children: any[]; + + constructor(attributes: Record = {}) { + this.children = []; + this.attributes = attributes; + } + + get render(): string { + return `${this.children.map(child => child.render).join("")}`; + } + + add(child: any): Span { + this.children.push(child); + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/text-area.spec.ts b/lib/clapton/javascripts/src/components/text-area.spec.ts new file mode 100644 index 0000000..1c61de0 --- /dev/null +++ b/lib/clapton/javascripts/src/components/text-area.spec.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest" +import { TextArea } from "./text-area" + +describe("TextArea", () => { + it("returns empty string if no params", () => { + expect(new TextArea({}, "foo").render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new TextArea({ foo: "bar" }, "foo", { id: "1", "data-foo": "bar" }).render).toBe(``) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new TextArea({ foo: "bar" }, "foo", { id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(``) + }) +}) diff --git a/lib/clapton/javascripts/src/components/text-area.ts b/lib/clapton/javascripts/src/components/text-area.ts new file mode 100644 index 0000000..8a8b3f2 --- /dev/null +++ b/lib/clapton/javascripts/src/components/text-area.ts @@ -0,0 +1,23 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class TextArea { + state: any; + attribute: string; + attributes: Record; + + constructor(state: any, attribute: string, attributes: Record = {}) { + this.state = state; + this.attributes = attributes; + this.attribute = attribute; + this.attributes["data-attribute"] = attribute; + } + + get render(): string { + return ``; + } + + add_action(event: string, klass: string, fn: string, options: { debounce?: number } = {}): TextArea { + this.attributes["data-action"] = `${this.attributes["data-action"] || ""} ${event}->${klass}#${fn}@${options.debounce || 0}`; + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/text-field.spec.ts b/lib/clapton/javascripts/src/components/text-field.spec.ts new file mode 100644 index 0000000..0baad67 --- /dev/null +++ b/lib/clapton/javascripts/src/components/text-field.spec.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from "vitest" +import { TextField } from "./text-field" + +describe("TextField", () => { + it("returns empty string if no params", () => { + expect(new TextField({}, "foo").render).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(new TextField({ foo: "bar" }, "foo", { id: "1", "data-foo": "bar" }).render).toBe(``) + }) + + it("returns attributes and data attributes with custom data attributes", () => { + expect(new TextField({ foo: "bar" }, "foo", { id: "1", data: { foo: { baz: "qux", quux: "corge" } } }).render).toBe(``) + }) +}) diff --git a/lib/clapton/javascripts/src/components/text-field.ts b/lib/clapton/javascripts/src/components/text-field.ts new file mode 100644 index 0000000..7bcfaed --- /dev/null +++ b/lib/clapton/javascripts/src/components/text-field.ts @@ -0,0 +1,23 @@ +import { htmlAttributes } from "../html/html-attributes"; + +export class TextField { + state: any; + attribute: string; + attributes: Record; + + constructor(state: any, attribute: string, attributes: Record = {}) { + this.state = state; + this.attributes = attributes; + this.attribute = attribute; + this.attributes["data-attribute"] = attribute; + } + + get render(): string { + return ``; + } + + add_action(event: string, klass: string, fn: string, options: { debounce?: number } = {}): TextField { + this.attributes["data-action"] = `${this.attributes["data-action"] || ""} ${event}->${klass}#${fn}@${options.debounce || 0}`; + return this; + } +} diff --git a/lib/clapton/javascripts/src/components/text.spec.ts b/lib/clapton/javascripts/src/components/text.spec.ts new file mode 100644 index 0000000..03c318f --- /dev/null +++ b/lib/clapton/javascripts/src/components/text.spec.ts @@ -0,0 +1,12 @@ +import { describe, it, expect } from "vitest" +import { Text } from "./text" + +describe("Text", () => { + it("returns empty string if no params", () => { + expect(new Text("").render).toBe("") + }) + + it("returns value", () => { + expect(new Text("bar").render).toBe("bar") + }) +}) diff --git a/lib/clapton/javascripts/src/components/text.ts b/lib/clapton/javascripts/src/components/text.ts new file mode 100644 index 0000000..70d1fd0 --- /dev/null +++ b/lib/clapton/javascripts/src/components/text.ts @@ -0,0 +1,11 @@ +export class Text { + value: string; + + constructor(value: string) { + this.value = value; + } + + get render(): string { + return this.value; + } +} diff --git a/lib/clapton/javascripts/src/dom/update-component.spec.ts b/lib/clapton/javascripts/src/dom/update-component.spec.ts new file mode 100644 index 0000000..0671692 --- /dev/null +++ b/lib/clapton/javascripts/src/dom/update-component.spec.ts @@ -0,0 +1,32 @@ +import { updateComponent } from "./update-component"; +import { describe, it, expect } from "vitest"; + +describe("updateComponent", () => { + it("updates the component", () => { + const component = document.createElement("div"); + component.innerHTML = `
                        `; + + const input = document.createElement("div"); + input.innerHTML = ``; + + class TestComponent { + state: any; + constructor(state: any) { + this.state = state; + } + get render() { + return `
                        ${this.state.testAttribute}
                        `; + } + }; + (window as any).TestComponent = TestComponent; + + updateComponent( + component.firstChild as HTMLElement, + JSON.parse(component.getAttribute("data-state") || "{}"), + "testAttribute", + input.firstChild as HTMLInputElement + ); + + expect(component.textContent).toBe("updated"); + }); +}); diff --git a/lib/clapton/javascripts/src/dom/update-component.ts b/lib/clapton/javascripts/src/dom/update-component.ts new file mode 100644 index 0000000..22bdcc1 --- /dev/null +++ b/lib/clapton/javascripts/src/dom/update-component.ts @@ -0,0 +1,10 @@ +import morphdom from "morphdom"; + +export const updateComponent = (component: HTMLElement, state: any, property: string, target: HTMLInputElement) => { + state[property] = target.value; + component.setAttribute("data-state", JSON.stringify(state)); + const componentName = component.getAttribute("data-component") as string; + const ComponentClass = window[componentName as any] as any; + const instance = new ComponentClass(state, component.dataset.id); + morphdom(component, instance.render); +}; diff --git a/lib/clapton/javascripts/src/html/html-attributes.spec.ts b/lib/clapton/javascripts/src/html/html-attributes.spec.ts new file mode 100644 index 0000000..81d7870 --- /dev/null +++ b/lib/clapton/javascripts/src/html/html-attributes.spec.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest' +import { htmlAttributes } from './html-attributes' + +describe('htmlAttributes', () => { + it("returns empty string if no params", () => { + expect(htmlAttributes({})).toBe("") + }) + + it("returns attributes and data attributes", () => { + expect(htmlAttributes({ id: "1", "data-foo": "bar" })).toBe(`id='1' data-foo='bar'`) + }) + + it('returns attributes and data attributes with custom data attributes', () => { + expect(htmlAttributes({ id: '1', data: { foo: 'bar' } })).toBe(`id='1' data-foo='bar'`) + expect(htmlAttributes({ id: '1', data: { foo: 'bar', baz: 'qux' } })).toBe(`id='1' data-foo='bar' data-baz='qux'`) + expect(htmlAttributes({ id: '1', data: { foo: { baz: 'qux', quux: 'corge' } } })).toBe(`id='1' data-foo-baz='qux' data-foo-quux='corge'`) + }) + + it('returns disabled if disabled is false', () => { + expect(htmlAttributes({ disabled: false })).toBe("") + expect(htmlAttributes({ disabled: true })).toBe("disabled") + }) + + it('escapes html', () => { + expect(htmlAttributes({ content: "" })).toBe(`content='<script>alert('xss')</script>'`) + }) +}) diff --git a/lib/clapton/javascripts/src/html/html-attributes.ts b/lib/clapton/javascripts/src/html/html-attributes.ts new file mode 100644 index 0000000..57690c1 --- /dev/null +++ b/lib/clapton/javascripts/src/html/html-attributes.ts @@ -0,0 +1,42 @@ +export const htmlAttributes = (params: Record) => { + const customDataAttributes = params.data || {} + const others = Object.keys(params).filter(key => key !== "data") + + const flattenDataAttributes = (data: Record, prefix = "data") => { + return Object.keys(data).reduce((acc, key) => { + const value = data[key] + if (typeof value === "object" && value !== null) { + acc.push(...flattenDataAttributes(value, `${prefix}-${key}`)) + } else { + acc.push(`${prefix}-${key}='${escapeHtml(value)}'`) + } + return acc + }, [] as string[]) + } + + return [ + others.map(key => { + if (key === "disabled") { + if (params[key] === false) { + return "" + } else { + return `${key}` + } + } + return `${key}='${escapeHtml(params[key])}'` + }).join(" "), + flattenDataAttributes(customDataAttributes).join(" ") + ].filter(Boolean).join(" ") +} + +const escapeHtml = (unsafe: string) => { + if (typeof unsafe !== "string") { + return "" + } + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/lib/clapton/javascripts/src/html/split-action-attribute.spec.ts b/lib/clapton/javascripts/src/html/split-action-attribute.spec.ts new file mode 100644 index 0000000..c9cd234 --- /dev/null +++ b/lib/clapton/javascripts/src/html/split-action-attribute.spec.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest' +import { splitActionAttribute } from './split-action-attribute' + +describe('splitActionAttribute', () => { + it("returns empty object if no action", () => { + expect(splitActionAttribute("")).toEqual({ + eventType: "", + componentName: "", + stateName: "", + fnName: "", + bounceTime: 0 + }) + }) + + it("returns eventType, componentName, stateName, fnName, bounceTime", () => { + expect(splitActionAttribute("click->ChatState#send_message@10")).toEqual({ eventType: "click", componentName: "ChatComponent", stateName: "ChatState", fnName: "send_message", bounceTime: 10 }) + }) +}) diff --git a/lib/clapton/javascripts/src/html/split-action-attribute.ts b/lib/clapton/javascripts/src/html/split-action-attribute.ts new file mode 100644 index 0000000..2644663 --- /dev/null +++ b/lib/clapton/javascripts/src/html/split-action-attribute.ts @@ -0,0 +1,6 @@ +export const splitActionAttribute = (action: string) => { + const match = action.match(/^(.+)->(.+)#(.+)@(\d+)$/); + const componentName = match?.[2].replace("State", "Component"); + if (!match) return { eventType: "", componentName: "", stateName: "", fnName: "", bounceTime: 0 }; + return { eventType: match[1], componentName: componentName, stateName: match[2], fnName: match[3], bounceTime: parseInt(match[4] || "0") }; +}; diff --git a/lib/clapton/javascripts/src/inputs/initialize-inputs.ts b/lib/clapton/javascripts/src/inputs/initialize-inputs.ts new file mode 100644 index 0000000..c24851e --- /dev/null +++ b/lib/clapton/javascripts/src/inputs/initialize-inputs.ts @@ -0,0 +1,16 @@ +import { updateComponent } from "dom/update-component"; + +export const initializeInputs = () => { + const inputElements = document.querySelectorAll("[data-attribute]"); + inputElements.forEach((element: any) => { + const attribute = element.getAttribute("data-attribute"); + const component = element.closest(`[data-component]`) as HTMLElement; + const state = JSON.parse(component.getAttribute("data-state") || "{}"); + if (!attribute || !component) return; + if (element.tagName === "INPUT") { + element.addEventListener("input", (event: Event) => { + updateComponent(component, state, attribute, event.target as HTMLInputElement); + }); + } + }); +}; diff --git a/lib/clapton/javascripts/src/utils/debounce.ts b/lib/clapton/javascripts/src/utils/debounce.ts new file mode 100644 index 0000000..cbac8a1 --- /dev/null +++ b/lib/clapton/javascripts/src/utils/debounce.ts @@ -0,0 +1,7 @@ +export const debounce = (fn: (...args: any[]) => void, delay: number) => { + let timer: NodeJS.Timeout | undefined = undefined; + return (...args: any[]) => { + clearTimeout(timer); + timer = setTimeout(() => fn(...args), delay); + }; +}; diff --git a/lib/clapton/javascripts/tsconfig.json b/lib/clapton/javascripts/tsconfig.json new file mode 100644 index 0000000..98887fb --- /dev/null +++ b/lib/clapton/javascripts/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": "./src", + "allowJs": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/lib/clapton/javascripts/vitest.config.js b/lib/clapton/javascripts/vitest.config.js new file mode 100644 index 0000000..83971b9 --- /dev/null +++ b/lib/clapton/javascripts/vitest.config.js @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'jsdom', + // 他の設定... + }, +}) diff --git a/lib/clapton/railtie.rb b/lib/clapton/railtie.rb new file mode 100644 index 0000000..fba6507 --- /dev/null +++ b/lib/clapton/railtie.rb @@ -0,0 +1,4 @@ +module Clapton + class Railtie < ::Rails::Railtie + end +end diff --git a/lib/clapton/state.rb b/lib/clapton/state.rb new file mode 100644 index 0000000..2eba414 --- /dev/null +++ b/lib/clapton/state.rb @@ -0,0 +1,25 @@ +module Clapton + class State + attr_reader :attributes + attr_accessor :errors + + def initialize(params = {}) + @attributes = params + @errors = [] + end + + def to_h + @attributes + end + + def self.attribute(attribute_name) + define_method(attribute_name) do + @attributes[attribute_name.to_sym] + end + + define_method("#{attribute_name}=") do |value| + @attributes[attribute_name.to_sym] = value + end + end + end +end diff --git a/lib/clapton/test_helper/base.rb b/lib/clapton/test_helper/base.rb new file mode 100644 index 0000000..f8cc011 --- /dev/null +++ b/lib/clapton/test_helper/base.rb @@ -0,0 +1,22 @@ +module Clapton + module TestHelper + module Base + require "execjs" + + def render_component(component, **kwargs) + js = File.read(File.join(__dir__, "..", "javascripts", "dist", "components.js")) + Dir.glob(Rails.root.join("app", "components", "**", "*.rb")).each do |file| + js += Ruby2JS.convert(File.read(file), preset: true) + js += "\n" + end + context = ExecJS.compile(js) + html = context.eval("new #{component.name.camelize}(#{kwargs.to_json}).render") + @page = Capybara.string(html) + end + + def page + @page + end + end + end +end diff --git a/lib/clapton/test_helper/minitest.rb b/lib/clapton/test_helper/minitest.rb new file mode 100644 index 0000000..96b3a44 --- /dev/null +++ b/lib/clapton/test_helper/minitest.rb @@ -0,0 +1,12 @@ +require "clapton/test_helper/base" + +module Clapton + module TestHelper + module Minitest + require "capybara/minitest" + + include Clapton::TestHelper::Base + include Capybara::Minitest::Assertions + end + end +end diff --git a/lib/clapton/test_helper/rspec.rb b/lib/clapton/test_helper/rspec.rb new file mode 100644 index 0000000..078a487 --- /dev/null +++ b/lib/clapton/test_helper/rspec.rb @@ -0,0 +1,12 @@ +require "clapton/test_helper/base" + +module Clapton + module TestHelper + module RSpec + require "capybara/rspec" + + include Clapton::TestHelper::Base + include Capybara::RSpecMatchers + end + end +end diff --git a/lib/clapton/version.rb b/lib/clapton/version.rb new file mode 100644 index 0000000..1f1381e --- /dev/null +++ b/lib/clapton/version.rb @@ -0,0 +1,3 @@ +module Clapton + VERSION = '0.1.0' +end diff --git a/lib/tasks/clapton_tasks.rake b/lib/tasks/clapton_tasks.rake new file mode 100644 index 0000000..50d2357 --- /dev/null +++ b/lib/tasks/clapton_tasks.rake @@ -0,0 +1,4 @@ +# desc "Explaining what the task does" +# task :clapton do +# # Task goes here +# end diff --git a/test/clapton/state_test.rb b/test/clapton/state_test.rb new file mode 100644 index 0000000..eaf4940 --- /dev/null +++ b/test/clapton/state_test.rb @@ -0,0 +1,33 @@ +require "test_helper" + +class TestState < Clapton::State + attribute :foo + attribute :bar + attribute :items +end + +class StateSpec < ActiveSupport::TestCase + test "can define state" do + state = TestState.new + assert_equal({}, state.attributes) + end + + test "can set and get multiple states" do + state = TestState.new + state.foo = "bar" + state.bar = "baz" + assert_equal("bar", state.foo) + assert_equal("baz", state.bar) + end + + test "can set attributes with a hash" do + state = TestState.new(foo: "bar", bar: "baz") + assert_equal({ foo: "bar", bar: "baz" }, state.to_h) + end + + test "can set array attributes" do + state = TestState.new(items: ["foo", "bar"]) + state.items << "baz" + assert_equal(["foo", "bar", "baz"], state.items) + end +end diff --git a/test/clapton_test.rb b/test/clapton_test.rb new file mode 100644 index 0000000..1ef9d67 --- /dev/null +++ b/test/clapton_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ClaptonTest < ActiveSupport::TestCase + test "it has a version number" do + assert Clapton::VERSION + end +end diff --git a/test/dummy/.rspec b/test/dummy/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/test/dummy/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile new file mode 100644 index 0000000..9a5ea73 --- /dev/null +++ b/test/dummy/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative "config/application" + +Rails.application.load_tasks diff --git a/test/dummy/app/assets/config/manifest.js b/test/dummy/app/assets/config/manifest.js new file mode 100644 index 0000000..5918193 --- /dev/null +++ b/test/dummy/app/assets/config/manifest.js @@ -0,0 +1,2 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css diff --git a/test/dummy/app/assets/images/.keep b/test/dummy/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/app/assets/stylesheets/application.css b/test/dummy/app/assets/stylesheets/application.css new file mode 100644 index 0000000..0ebd7fe --- /dev/null +++ b/test/dummy/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/test/dummy/app/channels/application_cable/channel.rb b/test/dummy/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/test/dummy/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/test/dummy/app/channels/application_cable/connection.rb b/test/dummy/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/test/dummy/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/test/dummy/app/components/.keep b/test/dummy/app/components/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/app/components/chat_component.rb b/test/dummy/app/components/chat_component.rb new file mode 100644 index 0000000..3eead73 --- /dev/null +++ b/test/dummy/app/components/chat_component.rb @@ -0,0 +1,9 @@ +class ChatComponent < Clapton::Component + def render + @state[:messages].each do |message| + @root.add(MessageComponent.new(role: message[:role], content: message[:content])) + end + @root.add(UserPromptComponent.new(role: "user", content: "")) + @root.render + end +end diff --git a/test/dummy/app/components/message_component.rb b/test/dummy/app/components/message_component.rb new file mode 100644 index 0000000..f2c1984 --- /dev/null +++ b/test/dummy/app/components/message_component.rb @@ -0,0 +1,6 @@ +class MessageComponent < Clapton::Component + def render + @root.add(Clapton::Text.new("#{@state[:role]}: #{@state[:content]}")) + @root.render + end +end diff --git a/test/dummy/app/components/task_item_component.rb b/test/dummy/app/components/task_item_component.rb new file mode 100644 index 0000000..71c44bc --- /dev/null +++ b/test/dummy/app/components/task_item_component.rb @@ -0,0 +1,16 @@ +class TaskItemComponent < Clapton::Component + def render + button = Clapton::Button.new + button.add(Clapton::Text.new(@state.done ? "✅" : "🟩")) + button.add_action(:click, :TaskListState, :toggle_done) + + text_field = Clapton::TextField.new(@state, :title, { "id": "task-title-#{@state.id}" }) + text_field.add_action(:input, :TaskListState, :update_title) + + datetime_field = Clapton::DateTimeField.new(@state, :due, { "id": "task-due-#{@state.id}" }) + datetime_field.add_action(:input, :TaskListState, :update_due) + + @root.add(button).add(text_field).add(datetime_field) + @root.render + end +end diff --git a/test/dummy/app/components/task_list_component.rb b/test/dummy/app/components/task_list_component.rb new file mode 100644 index 0000000..e9bd1a3 --- /dev/null +++ b/test/dummy/app/components/task_list_component.rb @@ -0,0 +1,12 @@ +class TaskListComponent < Clapton::Component + def render + @state.tasks.each do |task| + @root.add(TaskItemComponent.new(id: task[:id], title: task[:title], due: task[:due], done: task[:done])) + end + add_button = Clapton::Button.new + add_button.add(Clapton::Text.new("Add Task")) + add_button.add_action(:click, :TaskListState, :add_task) + @root.add(add_button) + @root.render + end +end diff --git a/test/dummy/app/components/user_form_component.rb b/test/dummy/app/components/user_form_component.rb new file mode 100644 index 0000000..11c7669 --- /dev/null +++ b/test/dummy/app/components/user_form_component.rb @@ -0,0 +1,9 @@ +class UserFormComponent < Clapton::Component + def render + text_field = Clapton::TextField.new(state, :name) + text_field.add_action(:input, :UserFormState, :save, { debounce: 500 }) + text_field.add_action(:input, :UserItemState, :update, { debounce: 500 }) + @root.add(text_field) + @root.render + end +end diff --git a/test/dummy/app/components/user_item_component.rb b/test/dummy/app/components/user_item_component.rb new file mode 100644 index 0000000..79e27a2 --- /dev/null +++ b/test/dummy/app/components/user_item_component.rb @@ -0,0 +1,10 @@ +class UserItemComponent < Clapton::Component + def render + link = Clapton::Link.new({ href: "/users/#{state[:id]}" }) + link.add(Clapton::Text.new(state[:name])) + form = UserFormComponent.new({ id: state[:id], name: state[:name], count: state[:count] }) + text = Clapton::Text.new(state[:count].to_s) + @root.add(link).add(form).add(text) + @root.render + end +end diff --git a/test/dummy/app/components/user_list_component.rb b/test/dummy/app/components/user_list_component.rb new file mode 100644 index 0000000..fae071e --- /dev/null +++ b/test/dummy/app/components/user_list_component.rb @@ -0,0 +1,8 @@ +class UserListComponent < Clapton::Component + def render + state[:users].each do |user| + @root.add(UserItemComponent.new(id: user[:id], name: user[:name], count: user[:count])) + end + @root.render + end +end diff --git a/test/dummy/app/components/user_prompt_component.rb b/test/dummy/app/components/user_prompt_component.rb new file mode 100644 index 0000000..f827091 --- /dev/null +++ b/test/dummy/app/components/user_prompt_component.rb @@ -0,0 +1,10 @@ +class UserPromptComponent < Clapton::Component + def render + text_field = Clapton::TextField.new(@state, :content) + button = Clapton::Button.new({ disabled: @state.content.empty? }) + button.add(Clapton::Text.new("Send")) + button.add_action(:click, :ChatState, :send) + @root.add(text_field).add(button) + @root.render + end +end diff --git a/test/dummy/app/controllers/application_controller.rb b/test/dummy/app/controllers/application_controller.rb new file mode 100644 index 0000000..09705d1 --- /dev/null +++ b/test/dummy/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/test/dummy/app/controllers/chat_controller.rb b/test/dummy/app/controllers/chat_controller.rb new file mode 100644 index 0000000..177e5ac --- /dev/null +++ b/test/dummy/app/controllers/chat_controller.rb @@ -0,0 +1,8 @@ +class ChatController < ApplicationController + def index + @components = [ + [:ChatComponent, { messages: [{ role: "assistant", content: "Hello" }] }], + [:ChatComponent, { messages: [{ role: "assistant", content: "Hello" }] }], + ] + end +end diff --git a/test/dummy/app/controllers/concerns/.keep b/test/dummy/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/app/controllers/tasks_controller.rb b/test/dummy/app/controllers/tasks_controller.rb new file mode 100644 index 0000000..2db550f --- /dev/null +++ b/test/dummy/app/controllers/tasks_controller.rb @@ -0,0 +1,8 @@ +class TasksController < ApplicationController + def index + @tasks = Task.all + @components = [ + [:TaskListComponent, { tasks: @tasks.map { |task| { id: task.id, title: task.title, due: task.due, done: task.done } } }] + ] + end +end diff --git a/test/dummy/app/controllers/users_controller.rb b/test/dummy/app/controllers/users_controller.rb new file mode 100644 index 0000000..c3c7acd --- /dev/null +++ b/test/dummy/app/controllers/users_controller.rb @@ -0,0 +1,7 @@ +class UsersController < ApplicationController + def index + @components = [ + [:UserListComponent, { users: User.all.map { |user| { id: user.id, name: user.name, count: user.name.length } } }] + ] + end +end diff --git a/test/dummy/app/helpers/application_helper.rb b/test/dummy/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/test/dummy/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/test/dummy/app/javascript/packs/application.js b/test/dummy/app/javascript/packs/application.js new file mode 100644 index 0000000..67ce467 --- /dev/null +++ b/test/dummy/app/javascript/packs/application.js @@ -0,0 +1,15 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. JavaScript code in this file should be added after the last require_* statement. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require rails-ujs +//= require activestorage +//= require_tree . diff --git a/test/dummy/app/jobs/application_job.rb b/test/dummy/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/test/dummy/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/test/dummy/app/mailers/application_mailer.rb b/test/dummy/app/mailers/application_mailer.rb new file mode 100644 index 0000000..286b223 --- /dev/null +++ b/test/dummy/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/test/dummy/app/models/application_record.rb b/test/dummy/app/models/application_record.rb new file mode 100644 index 0000000..10a4cba --- /dev/null +++ b/test/dummy/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/test/dummy/app/models/concerns/.keep b/test/dummy/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/app/models/task.rb b/test/dummy/app/models/task.rb new file mode 100644 index 0000000..3c23424 --- /dev/null +++ b/test/dummy/app/models/task.rb @@ -0,0 +1,2 @@ +class Task < ApplicationRecord +end diff --git a/test/dummy/app/models/user.rb b/test/dummy/app/models/user.rb new file mode 100644 index 0000000..379658a --- /dev/null +++ b/test/dummy/app/models/user.rb @@ -0,0 +1,2 @@ +class User < ApplicationRecord +end diff --git a/test/dummy/app/states/chat_state.rb b/test/dummy/app/states/chat_state.rb new file mode 100644 index 0000000..4830a49 --- /dev/null +++ b/test/dummy/app/states/chat_state.rb @@ -0,0 +1,28 @@ +class ChatState < Clapton::State + attribute :messages + + def send(params) + self.messages << { role: "user", content: params[:content] } + yield continue: true + + client = OpenAI::Client.new( + access_token: ENV.fetch("OPENAI_ACCESS_TOKEN"), + log_errors: true + ) + self.messages << { role: "assistant", content: "" } + client.chat( + parameters: { + model: "gpt-4o-mini", + messages: messages, + stream: proc do |chunk, _bytesize| + if chunk.dig("choices", 0, "finish_reason") == "stop" + yield continue: false + end + + self.messages.last[:content] << chunk.dig("choices", 0, "delta", "content") + yield continue: true + end + } + ) + end +end diff --git a/test/dummy/app/states/message_state.rb b/test/dummy/app/states/message_state.rb new file mode 100644 index 0000000..310dabd --- /dev/null +++ b/test/dummy/app/states/message_state.rb @@ -0,0 +1,4 @@ +class MessageState < Clapton::State + attribute :role + attribute :content +end diff --git a/test/dummy/app/states/task_item_state.rb b/test/dummy/app/states/task_item_state.rb new file mode 100644 index 0000000..16861d2 --- /dev/null +++ b/test/dummy/app/states/task_item_state.rb @@ -0,0 +1,6 @@ +class TaskItemState < Clapton::State + attribute :id + attribute :title + attribute :due + attribute :done +end diff --git a/test/dummy/app/states/task_list_state.rb b/test/dummy/app/states/task_list_state.rb new file mode 100644 index 0000000..60fd79b --- /dev/null +++ b/test/dummy/app/states/task_list_state.rb @@ -0,0 +1,27 @@ +class TaskListState < Clapton::State + attribute :tasks + + def add_task(params) + task = Task.create(title: "New Task", due: Date.today, done: false) + self.tasks << { id: task.id, title: task.title, due: task.due, done: task.done } + end + + def toggle_done(params) + task = Task.find(params[:id]) + task.update(done: !params[:done]) + self.tasks.find { |t| t[:id] == params[:id] }[:done] = task.done + end + + def update_title(params) + task = Task.find(params[:id]) + task.update(title: params[:title]) + self.tasks.find { |t| t[:id] == params[:id] }[:title] = task.title + end + + def update_due(params) + task = Task.find(params[:id]) + task.update(due: params[:due]) + self.tasks.find { |t| t[:id] == params[:id] }[:due] = task.due + end +end + diff --git a/test/dummy/app/states/user_form_state.rb b/test/dummy/app/states/user_form_state.rb new file mode 100644 index 0000000..4a08aa7 --- /dev/null +++ b/test/dummy/app/states/user_form_state.rb @@ -0,0 +1,10 @@ +class UserFormState < Clapton::State + attribute :id + attribute :name + + def save(params) + u = User.find(params[:id]) + u.name = params[:name] + u.save + end +end diff --git a/test/dummy/app/states/user_item_state.rb b/test/dummy/app/states/user_item_state.rb new file mode 100644 index 0000000..e9d0626 --- /dev/null +++ b/test/dummy/app/states/user_item_state.rb @@ -0,0 +1,10 @@ +class UserItemState < Clapton::State + attribute :id + attribute :name + attribute :count + + def update(params) + self.name = params[:name] + self.count = params[:name].length + end +end diff --git a/test/dummy/app/states/user_list_state.rb b/test/dummy/app/states/user_list_state.rb new file mode 100644 index 0000000..eebc3a4 --- /dev/null +++ b/test/dummy/app/states/user_list_state.rb @@ -0,0 +1,4 @@ +class UserListState < Clapton::State + attribute :users +end + diff --git a/test/dummy/app/states/user_prompt_state.rb b/test/dummy/app/states/user_prompt_state.rb new file mode 100644 index 0000000..270917e --- /dev/null +++ b/test/dummy/app/states/user_prompt_state.rb @@ -0,0 +1,4 @@ +class UserPromptState < Clapton::State + attribute :role + attribute :content +end diff --git a/test/dummy/app/views/chat/index.html.erb b/test/dummy/app/views/chat/index.html.erb new file mode 100644 index 0000000..c6a3271 --- /dev/null +++ b/test/dummy/app/views/chat/index.html.erb @@ -0,0 +1 @@ +<%= clapton_tag %> diff --git a/test/dummy/app/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb new file mode 100644 index 0000000..9e58dad --- /dev/null +++ b/test/dummy/app/views/layouts/application.html.erb @@ -0,0 +1,16 @@ + + + + Dummy + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag 'application', media: 'all' %> + <%= clapton_javascript_tag %> + + + + <%= yield %> + + diff --git a/test/dummy/app/views/layouts/mailer.html.erb b/test/dummy/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..cbd34d2 --- /dev/null +++ b/test/dummy/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/test/dummy/app/views/layouts/mailer.text.erb b/test/dummy/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/test/dummy/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/test/dummy/app/views/tasks/index.html.erb b/test/dummy/app/views/tasks/index.html.erb new file mode 100644 index 0000000..c6a3271 --- /dev/null +++ b/test/dummy/app/views/tasks/index.html.erb @@ -0,0 +1 @@ +<%= clapton_tag %> diff --git a/test/dummy/app/views/users/index.html.erb b/test/dummy/app/views/users/index.html.erb new file mode 100644 index 0000000..c6a3271 --- /dev/null +++ b/test/dummy/app/views/users/index.html.erb @@ -0,0 +1 @@ +<%= clapton_tag %> diff --git a/test/dummy/bin/rails b/test/dummy/bin/rails new file mode 100755 index 0000000..6fb4e40 --- /dev/null +++ b/test/dummy/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/test/dummy/bin/rake b/test/dummy/bin/rake new file mode 100755 index 0000000..4fbf10b --- /dev/null +++ b/test/dummy/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/test/dummy/bin/setup b/test/dummy/bin/setup new file mode 100755 index 0000000..5792302 --- /dev/null +++ b/test/dummy/bin/setup @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:prepare' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/test/dummy/config.ru b/test/dummy/config.ru new file mode 100644 index 0000000..4a3c09a --- /dev/null +++ b/test/dummy/config.ru @@ -0,0 +1,6 @@ +# This file is used by Rack-based servers to start the application. + +require_relative "config/environment" + +run Rails.application +Rails.application.load_server diff --git a/test/dummy/config/application.rb b/test/dummy/config/application.rb new file mode 100644 index 0000000..8aff6a5 --- /dev/null +++ b/test/dummy/config/application.rb @@ -0,0 +1,22 @@ +require_relative "boot" + +require "rails/all" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) +require "clapton" + +module Dummy + class Application < Rails::Application + config.load_defaults Rails::VERSION::STRING.to_f + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + end +end diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb new file mode 100644 index 0000000..91e5081 --- /dev/null +++ b/test/dummy/config/boot.rb @@ -0,0 +1,5 @@ +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) + +require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) +$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) diff --git a/test/dummy/config/cable.yml b/test/dummy/config/cable.yml new file mode 100644 index 0000000..98367f8 --- /dev/null +++ b/test/dummy/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: dummy_production diff --git a/test/dummy/config/database.yml b/test/dummy/config/database.yml new file mode 100644 index 0000000..c52c94d --- /dev/null +++ b/test/dummy/config/database.yml @@ -0,0 +1,16 @@ +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: db/development.sqlite3 + +test: + <<: *default + database: db/test.sqlite3 + +production: + <<: *default + database: db/production.sqlite3 diff --git a/test/dummy/config/environment.rb b/test/dummy/config/environment.rb new file mode 100644 index 0000000..cac5315 --- /dev/null +++ b/test/dummy/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/test/dummy/config/environments/development.rb b/test/dummy/config/environments/development.rb new file mode 100644 index 0000000..438743c --- /dev/null +++ b/test/dummy/config/environments/development.rb @@ -0,0 +1,76 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true +end diff --git a/test/dummy/config/environments/production.rb b/test/dummy/config/environments/production.rb new file mode 100644 index 0000000..abb1289 --- /dev/null +++ b/test/dummy/config/environments/production.rb @@ -0,0 +1,120 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain. + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). + config.log_level = :info + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "dummy_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Log disallowed deprecations. + config.active_support.disallowed_deprecation = :log + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Inserts middleware to perform automatic connection switching. + # The `database_selector` hash is used to pass options to the DatabaseSelector + # middleware. The `delay` is used to determine how long to wait after a write + # to send a subsequent read to the primary. + # + # The `database_resolver` class is used by the middleware to determine which + # database is appropriate to use based on the time delay. + # + # The `database_resolver_context` class is used by the middleware to set + # timestamps for the last write to the primary. The resolver uses the context + # class timestamps to determine how long to wait before reading from the + # replica. + # + # By default Rails will store a last write timestamp in the session. The + # DatabaseSelector middleware is designed as such you can define your own + # strategy for connection switching and pass that into the middleware through + # these configuration options. + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session +end diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb new file mode 100644 index 0000000..17ce39c --- /dev/null +++ b/test/dummy/config/environments/test.rb @@ -0,0 +1,59 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true +end diff --git a/test/dummy/config/initializers/application_controller_renderer.rb b/test/dummy/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000..89d2efa --- /dev/null +++ b/test/dummy/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/test/dummy/config/initializers/assets.rb b/test/dummy/config/initializers/assets.rb new file mode 100644 index 0000000..fe48fc3 --- /dev/null +++ b/test/dummy/config/initializers/assets.rb @@ -0,0 +1,12 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/test/dummy/config/initializers/backtrace_silencers.rb b/test/dummy/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000..33699c3 --- /dev/null +++ b/test/dummy/config/initializers/backtrace_silencers.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code +# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". +Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] diff --git a/test/dummy/config/initializers/content_security_policy.rb b/test/dummy/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..41c4301 --- /dev/null +++ b/test/dummy/config/initializers/content_security_policy.rb @@ -0,0 +1,28 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# If you are using UJS then enable automatic nonce generation +# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } + +# Set the nonce only to specific directives +# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/test/dummy/config/initializers/cookies_serializer.rb b/test/dummy/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000..5a6a32d --- /dev/null +++ b/test/dummy/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/test/dummy/config/initializers/filter_parameter_logging.rb b/test/dummy/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..4b34a03 --- /dev/null +++ b/test/dummy/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/test/dummy/config/initializers/inflections.rb b/test/dummy/config/initializers/inflections.rb new file mode 100644 index 0000000..ac033bf --- /dev/null +++ b/test/dummy/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/test/dummy/config/initializers/mime_types.rb b/test/dummy/config/initializers/mime_types.rb new file mode 100644 index 0000000..dc18996 --- /dev/null +++ b/test/dummy/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/test/dummy/config/initializers/permissions_policy.rb b/test/dummy/config/initializers/permissions_policy.rb new file mode 100644 index 0000000..00f64d7 --- /dev/null +++ b/test/dummy/config/initializers/permissions_policy.rb @@ -0,0 +1,11 @@ +# Define an application-wide HTTP permissions policy. For further +# information see https://developers.google.com/web/updates/2018/06/feature-policy +# +# Rails.application.config.permissions_policy do |f| +# f.camera :none +# f.gyroscope :none +# f.microphone :none +# f.usb :none +# f.fullscreen :self +# f.payment :self, "https://secure.example.com" +# end diff --git a/test/dummy/config/initializers/wrap_parameters.rb b/test/dummy/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000..bbfc396 --- /dev/null +++ b/test/dummy/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/test/dummy/config/locales/en.yml b/test/dummy/config/locales/en.yml new file mode 100644 index 0000000..cf9b342 --- /dev/null +++ b/test/dummy/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/test/dummy/config/puma.rb b/test/dummy/config/puma.rb new file mode 100644 index 0000000..d9b3e83 --- /dev/null +++ b/test/dummy/config/puma.rb @@ -0,0 +1,43 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +# +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb new file mode 100644 index 0000000..3012469 --- /dev/null +++ b/test/dummy/config/routes.rb @@ -0,0 +1,7 @@ +Rails.application.routes.draw do + mount Clapton::Engine => "/clapton" + + get "/users", to: "users#index" + get "/chat", to: "chat#index" + get "/tasks", to: "tasks#index" +end diff --git a/test/dummy/config/storage.yml b/test/dummy/config/storage.yml new file mode 100644 index 0000000..d32f76e --- /dev/null +++ b/test/dummy/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket + +# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/test/dummy/db/migrate/20241011052828_create_users.rb b/test/dummy/db/migrate/20241011052828_create_users.rb new file mode 100644 index 0000000..9f06756 --- /dev/null +++ b/test/dummy/db/migrate/20241011052828_create_users.rb @@ -0,0 +1,9 @@ +class CreateUsers < ActiveRecord::Migration[6.1] + def change + create_table :users do |t| + t.string :name + + t.timestamps + end + end +end diff --git a/test/dummy/db/migrate/20241012031613_create_tasks.rb b/test/dummy/db/migrate/20241012031613_create_tasks.rb new file mode 100644 index 0000000..61d746e --- /dev/null +++ b/test/dummy/db/migrate/20241012031613_create_tasks.rb @@ -0,0 +1,11 @@ +class CreateTasks < ActiveRecord::Migration[6.1] + def change + create_table :tasks do |t| + t.string :title + t.datetime :due + t.boolean :done + + t.timestamps + end + end +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb new file mode 100644 index 0000000..b712747 --- /dev/null +++ b/test/dummy/db/schema.rb @@ -0,0 +1,29 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2024_10_12_031613) do + + create_table "tasks", force: :cascade do |t| + t.string "title" + t.datetime "due" + t.boolean "done" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + + create_table "users", force: :cascade do |t| + t.string "name" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + +end diff --git a/test/dummy/lib/assets/.keep b/test/dummy/lib/assets/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/log/.keep b/test/dummy/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/public/404.html b/test/dummy/public/404.html new file mode 100644 index 0000000..2be3af2 --- /dev/null +++ b/test/dummy/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
                        +
                        +

                        The page you were looking for doesn't exist.

                        +

                        You may have mistyped the address or the page may have moved.

                        +
                        +

                        If you are the application owner check the logs for more information.

                        +
                        + + diff --git a/test/dummy/public/422.html b/test/dummy/public/422.html new file mode 100644 index 0000000..c08eac0 --- /dev/null +++ b/test/dummy/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
                        +
                        +

                        The change you wanted was rejected.

                        +

                        Maybe you tried to change something you didn't have access to.

                        +
                        +

                        If you are the application owner check the logs for more information.

                        +
                        + + diff --git a/test/dummy/public/500.html b/test/dummy/public/500.html new file mode 100644 index 0000000..78a030a --- /dev/null +++ b/test/dummy/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
                        +
                        +

                        We're sorry, but something went wrong.

                        +
                        +

                        If you are the application owner check the logs for more information.

                        +
                        + + diff --git a/test/dummy/public/apple-touch-icon-precomposed.png b/test/dummy/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/public/apple-touch-icon.png b/test/dummy/public/apple-touch-icon.png new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/public/favicon.ico b/test/dummy/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/test/dummy/spec/components/task_list_component_spec.rb b/test/dummy/spec/components/task_list_component_spec.rb new file mode 100644 index 0000000..b06be76 --- /dev/null +++ b/test/dummy/spec/components/task_list_component_spec.rb @@ -0,0 +1,8 @@ +require "rails_helper" + +RSpec.describe :TaskListComponent, type: :component do + it "renders the component" do + rendered = render_component(:TaskListComponent, tasks: [{ id: 1, title: "Task 1", done: false, due: Time.current }]) + expect(rendered).to have_selector("input[type='text']") + end +end diff --git a/test/dummy/spec/features/tasks/index_spec.rb b/test/dummy/spec/features/tasks/index_spec.rb new file mode 100644 index 0000000..5672521 --- /dev/null +++ b/test/dummy/spec/features/tasks/index_spec.rb @@ -0,0 +1,39 @@ +require "rails_helper" + +describe "Tasks", :js do + it "displays a task" do + Task.create!(title: "Task 1", done: false, due: Time.current) + visit tasks_path + expect(page).to have_content("🟩") + expect(page).to have_selector("input[type='text'][value='Task 1']") + expect(page).to have_content("Add Task") + end + + it "creates a task" do + visit tasks_path + click_on "Add Task" + expect(page).to have_selector("input[type='text'][value='New Task']") + end + + it "updates a task done status" do + Task.create!(title: "Task 1", done: false, due: Time.current) + visit tasks_path + click_on "🟩", match: :first + sleep 1 + expect(page).to have_content("✅") + end + + it "updates a task title" do + Task.create!(title: "Task 1", done: false, due: Time.current) + visit tasks_path + find("input[type='text'][value='Task 1']").fill_in with: "Task 2" + sleep 1 + expect(page).to have_selector("input[type='text'][value='Task 2']") + end + + it "updates a task due date" do + Task.create!(title: "Task 1", done: false, due: Time.current) + visit tasks_path + find("input[type='datetime-local'][id='task-due-#{Task.last.id}']").fill_in with: "2024-10-14T10:00" + end +end diff --git a/test/dummy/spec/rails_helper.rb b/test/dummy/spec/rails_helper.rb new file mode 100644 index 0000000..8f2aa35 --- /dev/null +++ b/test/dummy/spec/rails_helper.rb @@ -0,0 +1,65 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Rails.root.glob('spec/support/**/*.rb').sort_by(&:to_s).each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = Rails.root.join('spec/fixtures') + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/6-0/rspec-rails + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") + + config.include Clapton::TestHelper::RSpec, type: :component +end diff --git a/test/dummy/spec/spec_helper.rb b/test/dummy/spec/spec_helper.rb new file mode 100644 index 0000000..327b58e --- /dev/null +++ b/test/dummy/spec/spec_helper.rb @@ -0,0 +1,94 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/test/dummy/test/components/task_list_component_test.rb b/test/dummy/test/components/task_list_component_test.rb new file mode 100644 index 0000000..2d24cbc --- /dev/null +++ b/test/dummy/test/components/task_list_component_test.rb @@ -0,0 +1,10 @@ +require "test_helper" + +class TaskListComponentTest < ActiveSupport::TestCase + include Clapton::TestHelper::Minitest + + test "renders" do + render_component(:TaskListComponent, tasks: [{ id: 1, title: "Task 1", done: false, due: Time.current }]) + assert_selector "input[type='text']" + end +end diff --git a/test/dummy/test/test_helper.rb b/test/dummy/test/test_helper.rb new file mode 100644 index 0000000..47b598d --- /dev/null +++ b/test/dummy/test/test_helper.rb @@ -0,0 +1,13 @@ +ENV['RAILS_ENV'] ||= 'test' +require_relative "../config/environment" +require "rails/test_help" + +class ActiveSupport::TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... +end diff --git a/test/helpers/fuzzy_html_assertion.rb b/test/helpers/fuzzy_html_assertion.rb new file mode 100644 index 0000000..45c2357 --- /dev/null +++ b/test/helpers/fuzzy_html_assertion.rb @@ -0,0 +1,23 @@ +module FuzzyHtmlAssertion + def assert_fuzzy_html_equal(expected, actual, msg = nil) + expected_normalized = normalize_html(expected) + actual_normalized = normalize_html(actual) + + assert_equal(expected_normalized, actual_normalized, msg) + end + + private + + def normalize_html(html) + html + .gsub(/\s+/, ' ') + .gsub(/>\s+<') + .gsub(/"\s+/, '"') + .gsub(/'/, "\"") + .gsub(/\s+="/, '="') + .gsub(//m, '') + .gsub(/<([^>]+)>/) { |m| m.downcase } + .gsub(/\s\/>/, "/>") + .strip + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..9f348fb --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,17 @@ +# Configure Rails Environment +ENV["RAILS_ENV"] = "test" + +require_relative "../test/dummy/config/environment" +ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] +require "rails/test_help" + +require "rails/test_unit/reporter" +Rails::TestUnitReporter.executable = 'bin/test' + +# Load fixtures from the engine +if ActiveSupport::TestCase.respond_to?(:fixture_path=) + ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) + ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path + ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" + ActiveSupport::TestCase.fixtures :all +end