diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..3dbb2fe --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,27 @@ +name: Ruby + +on: + push: + branches: + - master + + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} + strategy: + matrix: + ruby: + - '3.2.4' + + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Make sure assets can compile in the dummy app + run: bundle exec rails app:assets:precompile diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..840e793 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,84 @@ +name: Release Gem + +on: + release: + types: [published] + +jobs: + build: + name: Build Gem + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Run NPM install + run: npm install + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + - name: Build gem + run: bundle exec rake build + - name: List gem + run: | + find pkg + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + path: pkg/*.gem + + test: + runs-on: ubuntu-latest + name: Test gem + needs: + - build + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Download artifact + uses: actions/download-artifact@v4 + with: + path: 'pkg' + - name: List gem + run: | + find pkg + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + - name: Install gem + run: | + gem install pkg/artifact/*.gem + + push: + name: Push Gem to Server + runs-on: ubuntu-latest + needs: + - test + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + - name: Download artifact + uses: actions/download-artifact@v4 + with: + path: pkg + - name: List gems + run: | + find pkg + - name: Set up GitHub Packages authentication + run: | + mkdir -p ~/.gem + cat > ~/.gem/credentials <<'CREDENTIALS' + --- + :github: Bearer ${{ secrets.GITHUB_TOKEN }} + CREDENTIALS + chmod 0600 ~/.gem/credentials + - name: Push gem + run: | + find pkg/artifact -name '*.gem' | while read -r gem; do + echo "=== pushing '${gem}'" + gem push --key github --host https://rubygems.pkg.github.com/hedgeyedev "${gem}" + done + - name: Clean up credentials + run: | + rm -rvf ~/.gem/credentials diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..8499e97 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Ruby + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + test: + + runs-on: ubuntu-latest + strategy: + matrix: + ruby-version: ['3.1', '3.2', '3.3'] + + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1.163.0 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true + - name: Run tests + run: bundle exec rails test diff --git a/.github_changelog_generator b/.github_changelog_generator new file mode 100644 index 0000000..955b22e --- /dev/null +++ b/.github_changelog_generator @@ -0,0 +1,4 @@ +project=phlex_preview +unreleased=false +future-release=0.2.0 +since-tag=0.1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ca49db --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +/.bundle/ +/doc/ +/log/*.log +/pkg/ +/tmp/ +/test/dummy/db/*.sqlite3 +/test/dummy/db/*.sqlite3-* +/test/dummy/log/*.log +/test/dummy/storage/ +/test/dummy/tmp/ +/test/dummy/public/assets/ +*.gem +coverage/ +.DS_Store +/.idea/ +/.ruby-lsp/ +app/assets/builds +bun.lockb +node_modules/ +package.json diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 0000000..058a5f0 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +phlex_preview diff --git a/.ruby-version b/.ruby-version index be94e6f..351227f 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2.2 +3.2.4 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/Gemfile b/Gemfile index 4dcd185..78b3eca 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,11 @@ -# frozen_string_literal: true - source "https://rubygems.org" +git_source(:github) { |repo| "https://github.com/#{repo}.git" } -# gem "rails" - -gem "phlex", "~> 1.10" -gem "roda", "~> 3.79" +gemspec -gem "rouge", "~> 4.2" +group :development do + gem "puma" + gem "sprockets-rails" + gem "foreman" + gem "github_changelog_generator", "~> 1.16" +end diff --git a/Gemfile.lock b/Gemfile.lock index b3be6a4..4d690be 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,20 +1,325 @@ +PATH + remote: . + specs: + phlex_storybook (0.1.0) + importmap-rails + phlex-icons (~> 0.11.0) + phlex-rails (~> 1.2) + rails (>= 7.2) + rouge (~> 4.3.0) + stimulus-rails + tailwindcss-rails (~> 2.7) + turbo-rails + turbo_power (~> 0.6.2) + GEM remote: https://rubygems.org/ specs: - phlex (1.10.2) - rack (3.0.10) - roda (3.79.0) - rack - rouge (4.2.1) + actioncable (7.2.1) + actionpack (= 7.2.1) + activesupport (= 7.2.1) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + zeitwerk (~> 2.6) + actionmailbox (7.2.1) + actionpack (= 7.2.1) + activejob (= 7.2.1) + activerecord (= 7.2.1) + activestorage (= 7.2.1) + activesupport (= 7.2.1) + mail (>= 2.8.0) + actionmailer (7.2.1) + actionpack (= 7.2.1) + actionview (= 7.2.1) + activejob (= 7.2.1) + activesupport (= 7.2.1) + mail (>= 2.8.0) + rails-dom-testing (~> 2.2) + actionpack (7.2.1) + actionview (= 7.2.1) + activesupport (= 7.2.1) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4, < 3.2) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actiontext (7.2.1) + actionpack (= 7.2.1) + activerecord (= 7.2.1) + activestorage (= 7.2.1) + activesupport (= 7.2.1) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.2.1) + activesupport (= 7.2.1) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activejob (7.2.1) + activesupport (= 7.2.1) + globalid (>= 0.3.6) + activemodel (7.2.1) + activesupport (= 7.2.1) + activerecord (7.2.1) + activemodel (= 7.2.1) + activesupport (= 7.2.1) + timeout (>= 0.4.0) + activestorage (7.2.1) + actionpack (= 7.2.1) + activejob (= 7.2.1) + activerecord (= 7.2.1) + activesupport (= 7.2.1) + marcel (~> 1.0) + activesupport (7.2.1) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + logger (>= 1.4.2) + minitest (>= 5.1) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + async (2.16.1) + console (~> 1.26) + fiber-annotation + io-event (~> 1.6, >= 1.6.5) + async-http (0.75.0) + async (>= 2.10.2) + async-pool (~> 0.7) + io-endpoint (~> 0.11) + io-stream (~> 0.4) + protocol-http (~> 0.30) + protocol-http1 (~> 0.20) + protocol-http2 (~> 0.18) + traces (>= 0.10) + async-http-faraday (0.19.0) + async-http (~> 0.42) + faraday + async-pool (0.8.1) + async (>= 1.25) + metrics + traces + base64 (0.2.0) + bigdecimal (3.1.8) + builder (3.3.0) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) + console (1.27.0) + fiber-annotation + fiber-local (~> 1.1) + json + crass (1.0.6) + date (3.3.4) + drb (2.2.1) + erubi (1.13.0) + faraday (2.11.0) + faraday-net_http (>= 2.0, < 3.4) + logger + faraday-http-cache (2.5.1) + faraday (>= 0.8) + faraday-net_http (3.3.0) + net-http + fiber-annotation (0.2.0) + fiber-local (1.1.0) + fiber-storage + fiber-storage (1.0.0) + foreman (0.88.1) + github_changelog_generator (1.16.4) + activesupport + async (>= 1.25.0) + async-http-faraday + faraday-http-cache + multi_json + octokit (~> 4.6) + rainbow (>= 2.2.1) + rake (>= 10.0) + globalid (1.2.1) + activesupport (>= 6.1) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + importmap-rails (2.0.1) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.7.2) + io-endpoint (0.13.1) + io-event (1.6.5) + io-stream (0.4.0) + irb (1.14.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + json (2.7.2) + logger (1.6.0) + 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) + metrics (0.10.2) + mini_mime (1.1.5) + minitest (5.25.1) + multi_json (1.15.0) + net-http (0.4.1) + uri + net-imap (0.4.15) + 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) + octokit (4.25.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) + phlex (1.10.3) + phlex-icons (0.11.0) + phlex (~> 1.10) + phlex-rails (1.2.1) + phlex (~> 1.10.0) + railties (>= 6.1, < 8) + protocol-hpack (1.5.0) + protocol-http (0.33.0) + protocol-http1 (0.22.0) + protocol-http (~> 0.22) + protocol-http2 (0.18.0) + protocol-hpack (~> 1.4) + protocol-http (~> 0.18) + psych (5.1.2) + stringio + public_suffix (6.0.1) + puma (6.4.2) + nio4r (~> 2.0) + racc (1.8.1) + rack (3.1.7) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails (7.2.1) + actioncable (= 7.2.1) + actionmailbox (= 7.2.1) + actionmailer (= 7.2.1) + actionpack (= 7.2.1) + actiontext (= 7.2.1) + actionview (= 7.2.1) + activejob (= 7.2.1) + activemodel (= 7.2.1) + activerecord (= 7.2.1) + activestorage (= 7.2.1) + activesupport (= 7.2.1) + bundler (>= 1.15.0) + railties (= 7.2.1) + 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 (7.2.1) + actionpack (= 7.2.1) + activesupport (= 7.2.1) + irb (~> 1.13) + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rainbow (3.1.1) + rake (13.2.1) + rdoc (6.7.0) + psych (>= 4.0.0) + reline (0.5.9) + io-console (~> 0.5) + rouge (4.3.0) + sawyer (0.9.2) + addressable (>= 2.3.5) + faraday (>= 0.17.3, < 3) + securerandom (0.3.1) + 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) + stimulus-rails (1.3.4) + railties (>= 6.0.0) + stringio (3.1.1) + tailwindcss-rails (2.7.3) + railties (>= 7.0.0) + tailwindcss-rails (2.7.3-aarch64-linux) + railties (>= 7.0.0) + tailwindcss-rails (2.7.3-arm-linux) + railties (>= 7.0.0) + tailwindcss-rails (2.7.3-arm64-darwin) + railties (>= 7.0.0) + tailwindcss-rails (2.7.3-x86_64-darwin) + railties (>= 7.0.0) + tailwindcss-rails (2.7.3-x86_64-linux) + railties (>= 7.0.0) + thor (1.3.2) + timeout (0.4.1) + traces (0.13.1) + turbo-rails (2.0.6) + actionpack (>= 6.0.0) + activejob (>= 6.0.0) + railties (>= 6.0.0) + turbo_power (0.6.2) + turbo-rails (>= 1.3.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uri (0.13.1) + useragent (0.16.10) + webrick (1.8.1) + websocket-driver (0.7.6) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.6.17) PLATFORMS - ruby - x86_64-darwin-20 + aarch64-linux + arm-linux + arm64-darwin + x86-linux + x86_64-darwin + x86_64-linux DEPENDENCIES - phlex (~> 1.10) - roda (~> 3.79) - rouge (~> 4.2) + foreman + github_changelog_generator (~> 1.16) + phlex_storybook! + puma + sprockets-rails BUNDLED WITH - 2.5.6 + 2.5.5 diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..fa35d41 --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +web: bin/rails server -b 0.0.0.0 +css: bin/rails app:tailwind_engine_watch diff --git a/README.md b/README.md index dcdc225..e91e284 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,28 @@ -# Purpose +# PhlexStorybook +Short description and motivation. -A quick and dirty phlex preview written because I was trying to understand some phlex functionality and just wanted to be able to see the changes easily +## Usage +How to use my plugin. -# Guiding principles -- Be simple -- Minimum Javascript/CSS load - - Was no JS at all until I added Codemirror to syntax highlight the ruby - - No CSS framework - - How long can we keep it like this - - ![image](https://github.com/hedgeyedev/phlex_preview/assets/13941/306f0df6-91c9-40e9-9a33-9d2c0eff419e) - - Though technically not true, there's some embedded css and js - -- Explore the perks of having everything Ruby and mostly server side (blog post forthcoming) - -# Aspirations -- Currently a standalone playground meant to be run locally -- Maybe to be converted a rack app/rubygem that I can mount to any Ruby app to preview one's phlex components from their app - - A poor man's Storybook - -# Running -- clone -- bundle install -- rackup (I use rerun to autoload in development) -- hit localhost:9292 -- Create the code and the invocation to be rendered - -# Specifying invocations -- For this app, and it's potential "poor man's storybook" I wanted to include a way to invoke the object and pass it data in the files themselves and came up with a the format - -``` ruby -# Sample invocation: -# All the setup you need -# TestComponent.new(stuff_I_setup) -# End Sample invocation +## Installation +Add this line to your application's Gemfile: +```ruby +gem "phlex_storybook" ``` -- example from app_layout_component.rb - -``` ruby -# Sample invocation: -# code = "class UserProfileComponent < Phlex::HTML\n def initialize(user)\n @user = user\n end\n\n def view_template\n div {\n h1 { @user.name }\n p { @user.email }\n }\n end\nend" -# params = "UserProfileComponent.new(User.new('John Doe', 'john@example.com')) " -# AppLayoutComponent.new(code, params) -# End Sample invocation +And then execute: +```bash +$ bundle ``` -- This serves a part documentation as well as as something to automatically put into the invocation portion to render -- Also serves the future "poor man's Storybook" feature to be able to auto preview all your components -- To be delightly recursive - - open the either of the rendered_results_preview_component.rb or app_layout_component.rb to see it render it's own components in itself - -# Caveats -- Initial version super not safe, only intended to be run in development, may explore sandboxing later, but trying to think about also being able to handle components that invoke other components, so I want to have it in same app. TBD +Or install it yourself as: +```bash +$ gem install phlex_storybook +``` -# Pics -## Edit component code, specify invocation -![image](https://github.com/hedgeyedev/phlex_preview/assets/13941/336bc4dd-caec-49b2-be89-2c437e178e57) +## Contributing +Contribution directions go here. -## See both rendered and raw html - meta example, rendering that component -![image](https://github.com/hedgeyedev/phlex_preview/assets/13941/fd9140d3-3c0b-41e5-a019-32186c58eca6) -## Actual html of that component -![image](https://github.com/hedgeyedev/phlex_preview/assets/13941/c127e5bd-7242-4c6e-a5a7-f6c70c3012dc) +## 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..4dbe116 --- /dev/null +++ b/Rakefile @@ -0,0 +1,8 @@ +require "bundler/setup" + +APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) + +load "rails/tasks/engine.rake" +load "rails/tasks/statistics.rake" + +require "bundler/gem_tasks" diff --git a/app.rb b/app.rb deleted file mode 100644 index 5f4c08d..0000000 --- a/app.rb +++ /dev/null @@ -1,62 +0,0 @@ -require 'roda' -require 'phlex' -require 'tempfile' -require 'uri' - -require_relative 'application_component' -require_relative 'app_layout_component' -require_relative 'rendered_results_preview_component' - -class PreviewApp < Roda - plugin :render - - route do |r| - r.root do - phlex_code = r.params['phlex_code'] - params_code = r.params['params'] - AppLayoutComponent.new(phlex_code, params_code).call - end - - r.post 'render' do - code = r.params['code'] - - # Create a temporary file to store the class definition - file = Tempfile.new(['phlex_preview', '.rb']) - begin - file.write(code) - file.close - - # Dynamically load the class from the file - load file.path - instance = eval r.params['params'] - - # Render the instance of the newly defined class - response['Content-Type'] = 'text/html' - html = instance.call - RenderedResultsPreviewComponent.new(html).call - - rescue => e - response.status = 422 - e.message + "\n" + e.backtrace.join("
\n") - ensure - file.unlink # Delete the temp file - end - end - - r.post 'load' do - pp r.params - tempfile = r.params['file'][:tempfile] - content = File.read(tempfile) - - # Extracting Phlex code - phlex_code = content - - # Extracting invocation parameters - params_match = content.match(/# Sample invocation:(.*?)# end Sample invocation/mi) - params_code = params_match[1].strip.gsub(/^#/, '') if params_match - puts "params_code = |#{params_code}|" - response.redirect "/?phlex_code=#{URI.encode_www_form_component(phlex_code)}¶ms=#{URI.encode_www_form_component(params_code)}" - - end - end -end diff --git a/app/assets/config/phlex_storybook_manifest.js b/app/assets/config/phlex_storybook_manifest.js new file mode 100644 index 0000000..fee476a --- /dev/null +++ b/app/assets/config/phlex_storybook_manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images/phlex_storybook .svg +//= link_directory ../../javascript/phlex_storybook .js +//= link_directory ../../javascript/phlex_storybook/controllers .js diff --git a/app/assets/images/phlex_storybook/.keep b/app/assets/images/phlex_storybook/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/stylesheets/phlex_storybook/application.tailwind.css b/app/assets/stylesheets/phlex_storybook/application.tailwind.css new file mode 100644 index 0000000..55f8d6a --- /dev/null +++ b/app/assets/stylesheets/phlex_storybook/application.tailwind.css @@ -0,0 +1,62 @@ +@import url("//fonts.googleapis.com/css2?family=Karla:ital,wght@0,200..800;1,200..800&display=swap"); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply bg-white text-gray-900; + @apply dark:bg-gray-900 dark:text-gray-300; + font-family: 'Karla', sans-serif; + } + + h1, h2, h3, h4, h5, h6 { + @apply scroll-m-20; + @apply dark:text-gray-300; + } + h1 { + @apply text-3xl font-bold leading-normal lg:leading-normal tracking-tight lg:text-4xl; + } + h2 { + @apply text-2xl font-semibold tracking-tight transition-colors first:mt-0; + } + h3 { + @apply text-xl font-semibold tracking-tight; + } + h4 { + @apply text-lg font-medium tracking-tight; + } + + fieldset { + @apply border border-slate-400 p-4 mb-2; + legend { + @apply px-2 font-semibold; + } + } + + .story-selector { + ul { + @apply mb-3 w-full; + + li { + @apply w-full; + + a { + @apply pl-2 block hover:bg-slate-900 hover:rounded-md; + } + } + } + } + + li.story-preview-active, li.story-code-active { + @apply rounded border-slate-200 bg-slate-900; + } +} + +.lineno { + line-height: 0.75rem; +} +.rouge-gutter { + @apply text-gray-600; +} diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/phlex_storybook/application_controller.rb b/app/controllers/phlex_storybook/application_controller.rb new file mode 100644 index 0000000..8648822 --- /dev/null +++ b/app/controllers/phlex_storybook/application_controller.rb @@ -0,0 +1,4 @@ +module PhlexStorybook + class ApplicationController < ActionController::Base + end +end diff --git a/app/controllers/phlex_storybook/stories_controller.rb b/app/controllers/phlex_storybook/stories_controller.rb new file mode 100644 index 0000000..4ee304c --- /dev/null +++ b/app/controllers/phlex_storybook/stories_controller.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module PhlexStorybook + class StoriesController < ApplicationController + layout -> { Layouts::ApplicationLayout } + + def index + respond_to do |format| + format.html { render Components::Stories::Index.new(story_components: story_components) } + end + end + + def show + respond_to do |format| + format.html do + story_id = params[:story_id] + render Components::Stories::Index.new( + story_components: story_components, + selected: story_component, + selected_story: story_id, + ) + end + + format.turbo_stream do + story_id = params[:story_id] + render turbo_stream: [ + turbo_stream.push_state(story_path(story_component, story_id: story_id)), + turbo_stream.replace( + "component_selector", + Components::Stories::ComponentSelector.new( + story_components: story_components, + selected: story_component, + selected_story: story_id + ), + ), + turbo_stream.replace( + "component_display", + Components::Stories::ComponentDisplay.new(story_component: story_component, story_id: story_id), + ), + turbo_stream.replace( + "component_properties", + Components::Stories::ComponentProperties.new( + story_component: story_component, + story_id: story_id, + ), + ), + ] + end + end + end + + def update + respond_to do |format| + format.turbo_stream do + story_id = params[:story_id] + render turbo_stream: [ + turbo_stream.replace( + "component_display", + Components::Stories::ComponentDisplay.new( + story_component: story_component, + story_id: story_id, + **story_component.transform_props(params[:props].permit!.to_h.symbolize_keys), + ), + ) + ] + end + end + end + + private + + def story_components + @story_components ||= PhlexStorybook.configuration.components + end + + def story_component + story_components.detect { |e| e.component_name == params[:id] } + end + end +end diff --git a/app/helpers/phlex_storybook/application_helper.rb b/app/helpers/phlex_storybook/application_helper.rb new file mode 100644 index 0000000..89c320d --- /dev/null +++ b/app/helpers/phlex_storybook/application_helper.rb @@ -0,0 +1,4 @@ +module PhlexStorybook + module ApplicationHelper + end +end diff --git a/app/javascript/phlex_storybook/application.js b/app/javascript/phlex_storybook/application.js new file mode 100644 index 0000000..6da88af --- /dev/null +++ b/app/javascript/phlex_storybook/application.js @@ -0,0 +1,5 @@ +// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails +import { Turbo } from '@hotwired/turbo' +import "controllers" +import TurboPower from 'turbo_power' +TurboPower.initialize(Turbo.StreamActions) diff --git a/app/javascript/phlex_storybook/controllers/application.js b/app/javascript/phlex_storybook/controllers/application.js new file mode 100644 index 0000000..1213e85 --- /dev/null +++ b/app/javascript/phlex_storybook/controllers/application.js @@ -0,0 +1,9 @@ +import { Application } from "@hotwired/stimulus" + +const application = Application.start() + +// Configure Stimulus development experience +application.debug = false +window.Stimulus = application + +export { application } diff --git a/app/javascript/phlex_storybook/controllers/copy_controller.js b/app/javascript/phlex_storybook/controllers/copy_controller.js new file mode 100644 index 0000000..6c63cce --- /dev/null +++ b/app/javascript/phlex_storybook/controllers/copy_controller.js @@ -0,0 +1,33 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["btn", "source"]; + + copy(event) { + event.preventDefault(); + const text = this.sourceTarget.textContent; + const originalHtml = event.target.htmlContent; + navigator.clipboard.writeText(text).then(() => { + this.btnTarget.querySelector('.clipboard').classList.add('hidden'); + this.btnTarget.querySelector('.check').classList.remove('hidden'); + setTimeout(() => { + this.btnTarget.querySelector('.clipboard').classList.remove('hidden'); + this.btnTarget.querySelector('.check').classList.add('hidden'); + }, 1000); + }).catch(err => { + console.error('Failed to copy text: ', err); + }); + } + + enable() { + this.btnTarget.disabled = false; + // this.btnTarget.querySelector('.clipboard').classList.remove("stroke-slate-500"); + // this.btnTarget.classList.add("text-slate-100"); + } + + disable() { + this.btnTarget.disabled = true; + // this.btnTarget.classList.add("text-slate-500"); + // this.btnTarget.classList.remove("text-slate-100"); + } +} diff --git a/app/javascript/phlex_storybook/controllers/index.js b/app/javascript/phlex_storybook/controllers/index.js new file mode 100644 index 0000000..54ad4ca --- /dev/null +++ b/app/javascript/phlex_storybook/controllers/index.js @@ -0,0 +1,11 @@ +// Import and register all your controllers from the importmap under controllers/* + +import { application } from "controllers/application" + +// Eager load all controllers defined in the import map under controllers/**/*_controller +import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" +eagerLoadControllersFrom("controllers", application) + +// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) +// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" +// lazyLoadControllersFrom("controllers", application) diff --git a/app/javascript/phlex_storybook/controllers/story_display_controller.js b/app/javascript/phlex_storybook/controllers/story_display_controller.js new file mode 100644 index 0000000..6b5907f --- /dev/null +++ b/app/javascript/phlex_storybook/controllers/story_display_controller.js @@ -0,0 +1,19 @@ +import { Controller } from "@hotwired/stimulus"; + +export default class extends Controller { + static targets = ["previewBtn", "codeBtn", "preview", "code"]; + + showCode() { + this.previewTarget.classList.add('hidden'); + this.codeTarget.classList.remove('hidden'); + this.previewBtnTarget.classList.remove('story-preview-active'); + this.codeBtnTarget.classList.add('story-code-active'); + } + + showPreview() { + this.previewTarget.classList.remove('hidden'); + this.codeTarget.classList.add('hidden'); + this.previewBtnTarget.classList.add('story-preview-active'); + this.codeBtnTarget.classList.remove('story-code-active'); + } +} diff --git a/app_layout_component.rb b/app_layout_component.rb deleted file mode 100644 index 16ad00d..0000000 --- a/app_layout_component.rb +++ /dev/null @@ -1,222 +0,0 @@ -require 'phlex' - -class AppLayoutComponent < ApplicationComponent - def initialize(phlex_code, params) - @phlex_code = phlex_code - @params = params - end - - def app_styles - style { - unsafe_raw <<~CSS - -body, html { - font-family: 'Helvetica Neue', Arial, sans-serif; - margin: 0; - padding: 0; - background: #f9f9f9; - color: #333; -} - -.container { - width: 90%; - margin: 0 auto; - padding: 20px; -} - -.textarea { - width: 100%; - height: 500px; - margin-top: 10px; - padding: 8px; - box-sizing: border-box; - border: 1px solid #ccc; - border-radius: 4px; - resize: none; - background-color: #fff; - font-family: 'Courier New', monospace; -} - -button { - padding: 10px 20px; - margin-top: 10px; - border: none; - border-radius: 4px; - background-color: #007BFF; - color: white; - cursor: pointer; - font-size: 16px; -} - -button:hover { - background-color: #0056b3; -} - -.section { - background: white; - border: 1px solid #ddd; - padding: 15px; - margin-top: 20px; - border-radius: 4px; -} - -h1 { - color: #333; - font-size: 24px; -} - -.output-pane { - background-color: #f4f4f4; - border: 1px solid #ccc; - padding: 10px; - margin-top: 20px; - overflow-x: auto; - white-space: pre-wrap; /* Ensures that spaces and line breaks are respected */ -} -textarea, iframe { width: 100%; height: 500px; margin-bottom: 20px; } -.params { height: 100px; margin-bottom: 20px; } - -CSS - } - end - def view_template - html do - head do - title { "Phlex Preview App" } - common_styles - app_styles - codemirror_includes - end - - body do - div(class: "container") do - form(action: "/load", method: "post", enctype: "multipart/form-data") do - whitespace - input(type: "file", name: "file", accept: ".rb") - whitespace - button(type: "submit") { "Load Phlex Component" } - end - h1 {"Phlex Preview App"} - div(class: "section") do - - form action: "/render", method: "post", target: "preview_frame" do - label(for: 'code') { "Phlex Code:"} - textarea( name: "code", id: :code, placeholder: "#Type your Phlex component code here...\nclass YourComponent < Phlex::HTML\n ...") { @phlex_code } - - label(for: 'params') {"How to invoke your component(include necessary params):"} - textarea(class: 'params', id: :params, name: "params", placeholder: "e.g., UserProfileComponent(User.new(name: 'John Doe'))") { @params } - - button( type: "submit") { "Render"} - end - end - - div(class: 'section output-pane') do - h2 {"Preview:"} - iframe name: "preview_frame", srcdoc: "Your rendered output will appear here..." - end - end - codemirror_attach - end - end - end - - def codemirror_includes - - comment { "CodeMirror CSS" } - link( - rel: "stylesheet", - href: - "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css" - ) - - comment { "CodeMirror JavaScript Library" } - script( - src: - "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js" - ) - - comment { "Ruby mode" } - script( - src: - "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/ruby/ruby.min.js" - ) - style { unsafe_raw <<~CSS -/* Basic styling for CodeMirror */ -.CodeMirror { - border: 1px solid #ccc; /* Adds a light grey border around the editor */ - padding: 10px; /* Adds padding inside the editor for better text alignment */ - background-color: #f7f7f7; /* A light background color */ - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; - height: auto; /* Adjust height as needed */ - min-height: 200px; /* Minimum height to ensure sufficient editing space */ -} - -/* Specific styling for focused editor */ -.CodeMirror-focused { - border-color: #blue; /* Highlight border when editor is focused */ - box-shadow: 0 0 5px rgba(81, 203, 238, 1); /* Soft blue glow around editor */ -} - - -#code + .CodeMirror, #invocation-textarea + .CodeMirror { - height: 500px !important; /* Use !important to override any inline styles */ -} - -#params + .CodeMirror { - height: 100px !important; /* Separate rule to set a different height */ -} - -CSS - } - end - - def codemirror_attach - script do - unsafe_raw <<~JS - document.addEventListener("DOMContentLoaded", function() { - CodeMirror.fromTextArea(document.getElementById('code'), { - lineNumbers: true, - mode: 'ruby', - theme: 'default', - foldGutter: true, - indentUnit: 2, - tabSize: 2, - indentWithTabs: false - }); - - CodeMirror.fromTextArea(document.getElementById('params'), { - lineNumbers: true, - mode: 'ruby', - theme: 'default', - indentUnit: 2, - tabSize: 2, - indentWithTabs: false - }); - }); - JS - end - end -end - -# Sample invocation: -# code = <<~RUBY -# class UserProfileComponent < Phlex::HTML -# def initialize(user) -# @user = user -# end - -# def view_template -# div { -# h1 { @user.name } -# p { @user.email } -# } -# end -# end -# RUBY -# params = <<~RUBY -# user = Object.new -# def user.name = 'John Doe' -# def user.email = 'john@example.com' -# RUBY -# AppLayoutComponent.new(code, params) -# End Sample invocation diff --git a/application_component.rb b/application_component.rb deleted file mode 100644 index 5130d57..0000000 --- a/application_component.rb +++ /dev/null @@ -1,27 +0,0 @@ -class ApplicationComponent < Phlex::HTML - def common_styles - style { - unsafe_raw <<-CSS - body, html { - font-family: 'Arial', sans-serif; - color: #333; - line-height: 1.6; - } - .rendered-results-preview, .raw-html { - margin: 20px; - padding: 10px; - border: 1px solid #ccc; - border-radius: 5px; - background-color: #fff; - overflow-x: auto; - } - .highlight { - background-color: #f4f4f4; - border: 1px dashed #ddd; - font-family: 'Courier New', monospace; - white-space: pre-wrap; - } - CSS - } - end -end diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000..eda330c --- /dev/null +++ b/bin/dev @@ -0,0 +1,11 @@ +#!/usr/bin/env sh + +if gem list --no-installed --exact --silent foreman; then + echo "Installing foreman..." + gem install foreman +fi + +# Default to port 3000 if not specified +export PORT="${PORT:-3000}" + +exec foreman start -f Procfile.dev "$@" diff --git a/bin/foreman b/bin/foreman new file mode 100755 index 0000000..90b50e2 --- /dev/null +++ b/bin/foreman @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'foreman' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) + +bundle_binstub = File.expand_path("bundle", __dir__) + +if File.file?(bundle_binstub) + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("foreman", "foreman") diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..d9e49b1 --- /dev/null +++ b/bin/rails @@ -0,0 +1,32 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails gems +# installed from the root of your application. + +ENGINE_ROOT = File.expand_path("..", __dir__) +ENGINE_PATH = File.expand_path("../lib/phlex_storybook/engine", __dir__) +APP_PATH = File.expand_path("../test/dummy/config/application", __dir__) + +# Set up gems listed in the Gemfile. +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) +require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) + +require "rails" +# Pick the frameworks you want: +# require "active_model/railtie" + +## require "active_job/railtie" +## require "active_record/railtie" +## require "active_storage/engine" + +require "action_controller/railtie" + +## require "action_mailer/railtie" +## require "action_mailbox/engine" +## require "action_text/engine" + +require "action_view/railtie" +require "action_cable/engine" +# require "rails/test_unit/railtie" + +require "rails/commands" +require "rails/engine/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..8d89fff --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +require "rake" +Rake.application.run diff --git a/config.ru b/config.ru deleted file mode 100644 index 1566098..0000000 --- a/config.ru +++ /dev/null @@ -1,2 +0,0 @@ -require './app' # Requires the application file -run PreviewApp.freeze.app diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000..681e255 --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,6 @@ +pin "phlex_storybook", to: "phlex_storybook/application.js", preload: true +pin "@hotwired/turbo", to: "turbo.min.js", preload: true +pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true +pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true +pin "turbo_power", to: "https://ga.jspm.io/npm:turbo_power@0.1.6/dist/index.js" +pin_all_from PhlexStorybook::Engine.root.join("app/javascript/phlex_storybook/controllers"), under: "controllers", to: "phlex_storybook/controllers" diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..20ae717 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,9 @@ +PhlexStorybook::Engine.routes.draw do + root to: 'stories#index' + + resources :stories, only: [:index, :show, :update] do + collection do + get :all + end + end +end diff --git a/config/tailwind.config.js b/config/tailwind.config.js new file mode 100644 index 0000000..9a05cf0 --- /dev/null +++ b/config/tailwind.config.js @@ -0,0 +1,25 @@ +const defaultTheme = require('tailwindcss/defaultTheme') + +module.exports = { + darkMode: 'class', + content: [ + './public/*.html', + './app/helpers/**/*.rb', + './app/javascript/**/*.js', + './app/views/**/*.{rb,erb,haml,html,slim}', + './lib/phlex_storybook/**/*.rb' + ], + theme: { + extend: { + fontFamily: { + sans: ['Karla', ...defaultTheme.fontFamily.sans], + }, + }, + }, + plugins: [ + require('@tailwindcss/forms'), + require('@tailwindcss/aspect-ratio'), + require('@tailwindcss/typography'), + require('@tailwindcss/container-queries'), + ] +} diff --git a/lib/phlex_storybook.rb b/lib/phlex_storybook.rb new file mode 100644 index 0000000..d26a4dd --- /dev/null +++ b/lib/phlex_storybook.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "phlex_storybook/version" +require "phlex_storybook/engine" +require "phlex_storybook/configuration" + +# require "zeitwerk" + +# loader = Zeitwerk::Loader.new +# loader.inflector = Zeitwerk::GemInflector.new(__FILE__) +# loader.push_dir(File.expand_path("../app", __dir__)) +# loader.setup + +module PhlexStorybook + class << self + attr_writer :configuration + + def configuration + @configuration ||= Configuration.new + end + + def configure + yield(configuration) if block_given? + end + end +end diff --git a/lib/phlex_storybook/application_component.rb b/lib/phlex_storybook/application_component.rb new file mode 100644 index 0000000..22bdc4a --- /dev/null +++ b/lib/phlex_storybook/application_component.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module PhlexStorybook + class ApplicationComponent < Phlex::HTML + include Phlex::Rails::Helpers::Routes + include Phlex::Rails::Helpers::TurboFrameTag + + if Rails.env.development? + def before_template + # comment { "Before #{self.class.name}" } + super + end + end + end +end diff --git a/lib/phlex_storybook/application_view.rb b/lib/phlex_storybook/application_view.rb new file mode 100644 index 0000000..29612ea --- /dev/null +++ b/lib/phlex_storybook/application_view.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +module PhlexStorybook + class ApplicationView < ApplicationComponent + include Phlex::Rails::Helpers::CheckBox + include Phlex::Rails::Helpers::FormFor + include Phlex::Rails::Helpers::ImageTag + include Phlex::Rails::Helpers::StylesheetLinkTag + include Phlex::Rails::Helpers::JavascriptImportmapTags + include Phlex::Rails::Helpers::JavascriptIncludeTag + include Phlex::Rails::Helpers::AssetPath + include Phlex::Rails::Helpers::PathToAsset + include Phlex::Rails::Helpers::Request + include Phlex::Rails::Helpers::Tag + include ApplicationHelper + end +end diff --git a/lib/phlex_storybook/components/stories/component_display.rb b/lib/phlex_storybook/components/stories/component_display.rb new file mode 100644 index 0000000..6f882e9 --- /dev/null +++ b/lib/phlex_storybook/components/stories/component_display.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +module PhlexStorybook + module Components + module Stories + class ComponentDisplay < ApplicationView + include Phlex::Rails::Helpers::FieldSetTag + + def initialize(story_component:, story_id: nil, **props) + @story_component = story_component + @story_id = story_id + @props = props + end + + def view_template + if @story_component.nil? + turbo_frame_tag("component_display") do + blank_template + end + return + end + + turbo_frame_tag("component_display") do + div(class: "flex flex-col h-screen max-h-screen", data: { controller: "story-display copy" }) do + render_header @story_component.component_name + div(class: "px-2 flex-1 overflow-y-scroll overflow-x-hidden") do + props = @props.blank? ? @story_component.story_for(@story_id) : @props + if @story_id && props.nil? + h1 { "Story not found" } + next + end + + div(class: "mb-4") { @story_component.component_description } + + if @story_id + div(class: "container w-full h-fit min-w-0 mr-0") do + render_story_header + render ComponentRendering.new( + story_component: @story_component, + story_id: @story_id, + **props.except(:title).map.with_object({}) { |(k, v), h| h[k] = v } + ) + end + elsif @story_component.component_stories.present? + div { "Select a story from the left to see its usage..." } + else + div(class: "container w-full h-fit min-w-0 mr-0") do + render_story_header + render ComponentRendering.new(story_component: @story_component) + end + end + end + end + end + end + + private + + def blank_template + render_header("Select a component") + div(class: "px-2") { "Select a component from the left to see its details" } + end + + def render_header(text) + h2(class: "bg-slate-900 text-white border-x border-slate-700 p-2 flex-none") { text } + end + + def render_story_header + div(class: "flex justify-between") do + ul(class: "text-sm text-white inline-flex bg-slate-700 rounded border-slate-200") do + li( + class: "relative px-3 py-2 flex-grow-1 story-preview-active", + data: { + action: "click->story-display#showPreview click->copy#disable:prevent", + story_display_target: "previewBtn", + }, + ) do + button(class: "font-semibold") { "Preview" } + end + + li( + class: "relative px-3 py-2 flex-grow-1", + data: { + action: 'click->story-display#showCode click->copy#enable:prevent', + story_display_target: 'codeBtn', + }, + ) do + button(class: "font-semibold") { "Ruby Code" } + end + end + button( + disabled: true, + data: { copy_target: "btn", action: "click->copy#copy" }, + class: "bg-slate-700 group px-2 py-1 rounded", + ) do + span(class: 'clipboard') do + render Phlex::Icons::Lucide::Copy.new( + classes: "group-enabled:stroke-slate-100 group-disabled:stroke-slate-500 size-5", + ) + end + span(class: 'check hidden') do + render Phlex::Icons::Lucide::CopyCheck.new(classes: "stroke-green-400 size-5") + end + end + end + end + end + end + end +end diff --git a/lib/phlex_storybook/components/stories/component_properties.rb b/lib/phlex_storybook/components/stories/component_properties.rb new file mode 100644 index 0000000..e6a975e --- /dev/null +++ b/lib/phlex_storybook/components/stories/component_properties.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module PhlexStorybook + module Components + module Stories + class ComponentProperties < ApplicationView + def initialize(story_component:, story_id: nil) + @story_component = story_component + @story_id = story_id + + if story_id + @props = story_component.story_for(story_id) + end + end + + def view_template + if @story_component&.component_props.blank? + turbo_frame_tag("component_properties") do + h2(class: "bg-slate-900 p-2") { "Properties" } + div(class: "px-2") { "No properties" } + end + return + end + + turbo_frame_tag("component_properties") do + h2(class: "bg-slate-900 p-2") { "Properties" } + div(class: "px-2") do + form(action: helpers.story_path(@story_component, story_id: @story_id), method: 'PUT') do + ul do + @story_component.component_props.each do |prop| + li do + label(class: 'grid grid-cols-1 w-full') do + div { prop.label || prop.key.to_s.capitalize } + render prop.clone_with_value(@props&.fetch(prop.key, nil)) + end + end + end + end + + button( + type: "submit", + class: "mt-4 px-3 py-2 rounded-lg bg-slate-600 hover:ring-1 hover:ring-slate-100", + ) { "« Render" } + end + end + end + end + end + end + end +end diff --git a/lib/phlex_storybook/components/stories/component_rendering.rb b/lib/phlex_storybook/components/stories/component_rendering.rb new file mode 100644 index 0000000..17f5e98 --- /dev/null +++ b/lib/phlex_storybook/components/stories/component_rendering.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module PhlexStorybook + module Components + module Stories + class ComponentRendering < ApplicationView + def initialize(story_component:, story_id: nil, **props) + @story_component = story_component + @story_id = story_id + @props = props.blank? ? story_component.default_story : props + end + + def view_template + turbo_frame_tag("story-rendering") do + div(data: { story_display_target: "preview" }) do + render @story_component.component.new(**@props) + end + + div( + class: "hidden mt-4 w-full", + data: { story_display_target: "code" }, + ) do + div(id: "component-code", class: "p-2 overflow-auto scroll") do + source = @props.blank? ? "render #{@story_component.component.name}.new" : <<~RUBY.strip + render #{@story_component.component.name}.new( + #{@props.map { |k, v| "#{k}: #{v.inspect}," }.join("\n ")} + ) + RUBY + pre(class: "hidden", data: { copy_target: "source" }) { source } + formatter = Rouge::Formatters::HTMLLineTable.new(Rouge::Formatters::HTML.new) + lexer = Rouge::Lexers::Ruby.new + unsafe_raw formatter.format(lexer.lex(source)) + end + style do + unsafe_raw Rouge::Themes::Molokai.render(scope: '#component-code') + end + end + end + end + end + end + end +end diff --git a/lib/phlex_storybook/components/stories/component_selector.rb b/lib/phlex_storybook/components/stories/component_selector.rb new file mode 100644 index 0000000..b85a920 --- /dev/null +++ b/lib/phlex_storybook/components/stories/component_selector.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module PhlexStorybook + module Components + module Stories + class ComponentSelector < ApplicationView + def initialize(story_components:, selected: nil, selected_story: nil) + @story_components_by_category = story_components.group_by(&:component_category).sort + @selected = selected + @selected_story = selected_story + end + + def view_template + turbo_frame_tag("component_selector") do + h2(class: "bg-slate-900 p-2") { "Components" } + div(class: "px-2") do + @story_components_by_category.each do |category, story_components| + h4 { category } + ul do + story_components.each do |story_component| + li do + component_link(story_component) + + if story_component.component_stories.present? + ul do + story_component.component_stories&.each do |props| + li(class: "pl-4 text-sm") { story_link(story_component, props) } + end + end + end + end + end + end + end + end + end + end + + private + + def active_selection + 'font-semibold text-sky-500' + end + + def component_link(story_component) + active = story_component == @selected + a( + data: { turbo_stream: true }, + href: helpers.story_path(story_component.component_name), + class: "#{active ? active_selection : ''}", + ) do + span(class: "pr-1") do + render Phlex::Icons::Lucide::Component.new(classes: "#{icon_color(active)} size-4 inline") + end + span { story_component.component_name } + end + end + + def icon_color(state) + state ? 'stroke-sky-500' : 'stroke-slate-300' + end + + def story_link(story_component, props) + title = props.delete :title + id = story_component.id_for(title) + active = id == @selected_story + icon = active ? Phlex::Icons::Lucide::NotebookText : Phlex::Icons::Lucide::Notebook + + a( + class: "#{active ? active_selection : ''}", + href: helpers.story_path(story_component, story_id: id), + data: { turbo_stream: true }, + ) do + span(class: "pr-1") { render icon.new(classes: "#{icon_color(active)} size-4 inline") } + span { "#{title}"} + end + end + end + end + end +end diff --git a/lib/phlex_storybook/components/stories/index.rb b/lib/phlex_storybook/components/stories/index.rb new file mode 100644 index 0000000..5cb7979 --- /dev/null +++ b/lib/phlex_storybook/components/stories/index.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module PhlexStorybook + module Components + module Stories + class Index < ApplicationView + def initialize(story_components:, selected: nil, selected_story: nil) + @story_components = story_components + @selected = selected + @selected_story = selected_story + end + + def view_template + turbo_frame_tag("story_components") do + div class: "flex h-screen w-screen" do + div class: "flex-none w-1/4 min-w-40 max-w-80 bg-slate-700 text-white story-selector" do + render ComponentSelector.new(story_components: @story_components, selected: @selected, selected_story: @selected_story) + end + + div class: "flex-auto h-full w-1/2 bg-white" do + render ComponentDisplay.new(story_component: @selected, story_id: @selected_story) + end + + div class: "flex-none w-1/4 min-w-40 max-w-80 bg-slate-700 text-white" do + render ComponentProperties.new(story_component: @selected, story_id: @selected_story) + end + end + end + end + end + end + end +end diff --git a/lib/phlex_storybook/configuration.rb b/lib/phlex_storybook/configuration.rb new file mode 100644 index 0000000..9ebe58d --- /dev/null +++ b/lib/phlex_storybook/configuration.rb @@ -0,0 +1,25 @@ +module PhlexStorybook + class Configuration + attr_accessor :component_directories + + def initialize + @component_directories = %w[app/components] + end + + def components + component_directories.flat_map do |dir| + Dir.glob("#{dir}/**/*.rb").select { |f| File.file?(f) }.map do |file| + StoryComponent.new File.basename(file, ".rb").camelize.constantize, file + end + end + end + + def component_directories + @component_directories.map { |dir| Rails.root.join dir } + end + + def component_directories=(directories) + @component_directories = directories + end + end +end diff --git a/lib/phlex_storybook/engine.rb b/lib/phlex_storybook/engine.rb new file mode 100644 index 0000000..e653ca5 --- /dev/null +++ b/lib/phlex_storybook/engine.rb @@ -0,0 +1,28 @@ +require "importmap-rails" +require "turbo-rails" +require "stimulus-rails" + +require "phlex" +require "phlex_icons" +require "phlex-rails" +require "rouge" +require "turbo_power" +# require "phlex_ui" + +module PhlexStorybook + class Engine < ::Rails::Engine + isolate_namespace PhlexStorybook + + config.autoload_paths << "#{root}/lib" + + initializer "phlex_storybook.assets" do |app| + app.config.assets.paths << root.join("app/javascript") + app.config.assets.precompile += %w[ phlex_storybook_manifest ] + end + + initializer "phlex_storybook.importmap", before: "importmap" do |app| + app.config.importmap.paths << root.join("config/importmap.rb") + app.config.importmap.cache_sweepers << root.join("app/javascript") + end + end +end diff --git a/lib/phlex_storybook/layouts/application_layout.rb b/lib/phlex_storybook/layouts/application_layout.rb new file mode 100644 index 0000000..08ceb9e --- /dev/null +++ b/lib/phlex_storybook/layouts/application_layout.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module PhlexStorybook + module Layouts + class ApplicationLayout < ApplicationView + include Phlex::Rails::Layout + include Phlex::Rails::Helpers::CSRFMetaTags + include Phlex::Rails::Helpers::CSPMetaTag + include Phlex::Rails::Helpers::TurboRefreshesWith + + def view_template(&) + doctype + + html lang: "en" do + head do + meta charset: "UTF-8" + meta name: "viewport", content: "width=device-width, initial-scale=1.0" + title { "Phlex Component Preview" } + csrf_meta_tags + csp_meta_tag + javascript_importmap_tags "phlex_storybook" + stylesheet_link_tag "phlex_storybook_application", media: "all" + turbo_refreshes_with method: :morph, scroll: :preserve + end + body class: "bg-gray-100 text-gray-900 dark:bg-gray-900 dark:text-gray-100" do + yield + end + end + end + end + + def identifier + "phlex_storybook" + end + end +end diff --git a/lib/phlex_storybook/props/base.rb b/lib/phlex_storybook/props/base.rb new file mode 100644 index 0000000..897c7d8 --- /dev/null +++ b/lib/phlex_storybook/props/base.rb @@ -0,0 +1,30 @@ +module PhlexStorybook + module Props + class Base < ApplicationView + attr_reader :default, :key, :label, :placeholder, :required, :value + + def initialize(key:, default: nil, label: nil, placeholder: nil, required: false) + @key = key + @default = default + @label = label + @placeholder = placeholder + @required = required + @value = nil + end + + def transform(value) + value.blank? ? default : value + end + + def view_template(&block) + raise "Subclass responsibility" + end + + def clone_with_value(v) + p = dup + p.instance_variable_set(:@value, v) + p + end + end + end +end diff --git a/lib/phlex_storybook/props/boolean.rb b/lib/phlex_storybook/props/boolean.rb new file mode 100644 index 0000000..1e94722 --- /dev/null +++ b/lib/phlex_storybook/props/boolean.rb @@ -0,0 +1,20 @@ +module PhlexStorybook + module Props + class Boolean < Base + def self.default = false + + def transform(value) + Array(value).include? "1" + end + + def view_template + check_box( + "props[#{key}]", + nil, + required: required, + checked: value, + ) + end + end + end +end diff --git a/lib/phlex_storybook/props/select.rb b/lib/phlex_storybook/props/select.rb new file mode 100644 index 0000000..60047ee --- /dev/null +++ b/lib/phlex_storybook/props/select.rb @@ -0,0 +1,36 @@ +module PhlexStorybook + module Props + class Select < Base + attr_reader :include_blank, :multiple, :options + + def self.default = [] + + def initialize(key:, options:, label: nil, include_blank: false, multiple: false, placeholder: nil, required: nil) + super(key: key, label: label, placeholder: placeholder, required: required) + @include_blank = include_blank + @multiple = multiple + @options = options + end + + def view_template + select( + class: 'w-full text-slate-700', + name: name, + required: required, + multiple: multiple, + ) do + option(value: "", selected: Array(value).empty?) { "Select..." } if include_blank + options.each do |option| + option(value: option, selected: Array(value).include?(option)) { option } + end + end + end + + def name + return "props[#{key}]" unless multiple + + "props[#{key}][]" + end + end + end +end diff --git a/lib/phlex_storybook/props/string.rb b/lib/phlex_storybook/props/string.rb new file mode 100644 index 0000000..4676bb2 --- /dev/null +++ b/lib/phlex_storybook/props/string.rb @@ -0,0 +1,18 @@ +module PhlexStorybook + module Props + class String < Base + def self.default = "" + + def view_template + input( + class: 'w-full text-slate-700', + name: "props[#{key}]", + placeholder: placeholder, + required: required, + type: "text", + value: value, + ) + end + end + end +end diff --git a/lib/phlex_storybook/props/text.rb b/lib/phlex_storybook/props/text.rb new file mode 100644 index 0000000..b52b47f --- /dev/null +++ b/lib/phlex_storybook/props/text.rb @@ -0,0 +1,17 @@ +module PhlexStorybook + module Props + class Text < String + def self.default = "" + + def view_template + textarea( + class: 'w-full text-slate-700', + name: "props[#{key}]", + placeholder: placeholder, + required: required, + rows: 5, + ) { value } + end + end + end +end diff --git a/lib/phlex_storybook/story_component.rb b/lib/phlex_storybook/story_component.rb new file mode 100644 index 0000000..e667766 --- /dev/null +++ b/lib/phlex_storybook/story_component.rb @@ -0,0 +1,57 @@ +module PhlexStorybook + class StoryComponent + attr_reader :component, :location + + def initialize(component, location) + @component = component + @location = location + end + + def component_category + component.respond_to?(:component_category) ? component.component_category : "Uncategorized" + end + + def component_description + component.respond_to?(:component_description) ? component.component_description : "No description provided" + end + + def component_name + component.respond_to?(:component_name) ? component.component_name : component.name + end + alias to_param component_name + + def component_props + component.respond_to?(:component_props) ? component.component_props : [] + end + + def component_stories + component.respond_to?(:component_stories) ? component.component_stories : [] + end + + def default_story + component_props + .select { |prop| prop.required } + .map { |prop| [prop.key, prop.default || prop.class.default] } + .to_h + end + + def default_string = "" + def default_string_list = [] + + def id_for(title) + Digest::MD5.hexdigest(title) + end + + def story_for(id) + component_stories.detect { |props| id_for(props[:title]) == id } + end + + def transform_props(props) + return props if component_props.blank? + + props.map.with_object({}) do |(k, v), h| + h[k] = component_props.detect { |prop| prop.key == k }&.transform(v) + end.compact + end + end +end diff --git a/lib/phlex_storybook/version.rb b/lib/phlex_storybook/version.rb new file mode 100644 index 0000000..d601dd7 --- /dev/null +++ b/lib/phlex_storybook/version.rb @@ -0,0 +1,3 @@ +module PhlexStorybook + VERSION = "0.1.0" +end diff --git a/lib/tasks/css_task.rake b/lib/tasks/css_task.rake new file mode 100644 index 0000000..9c6010e --- /dev/null +++ b/lib/tasks/css_task.rake @@ -0,0 +1,9 @@ +task :tailwind_engine_watch do + require "tailwindcss-rails" + # NOTE: tailwindcss-rails is an engine + system "#{Tailwindcss::Engine.root.join("exe/tailwindcss")} \ + -i #{PhlexStorybook::Engine.root.join("app/assets/stylesheets/phlex_storybook/application.tailwind.css")} \ + -o #{PhlexStorybook::Engine.root.join("app/assets/builds/phlex_storybook_application.css")} \ + -c #{PhlexStorybook::Engine.root.join("config/tailwind.config.js")} \ + -w" +end diff --git a/lib/tasks/phlex_storybook_tasks.thor b/lib/tasks/phlex_storybook_tasks.thor new file mode 100644 index 0000000..f3ea215 --- /dev/null +++ b/lib/tasks/phlex_storybook_tasks.thor @@ -0,0 +1,15 @@ +# desc "Explaining what the task does" +# task :phlex_storybook do +# # Task goes here +# end + +class PhlexStorybook < Thor + include Thor::Actions + + desc "install", "Install Phlex Storybook" + def install + say "Hi! I'm going to install Phlex Storybook for you." + exec "bin/importmap pin phlex_ui" + append_to_file("app/javascript/application.js", "import 'phlex_ui';\n") + end +end diff --git a/phlex_storybook.gemspec b/phlex_storybook.gemspec new file mode 100644 index 0000000..b4f463c --- /dev/null +++ b/phlex_storybook.gemspec @@ -0,0 +1,37 @@ +require_relative "lib/phlex_storybook/version" + +Gem::Specification.new do |spec| + spec.name = "phlex_storybook" + spec.version = PhlexStorybook::VERSION + spec.authors = [ "Hedgeye Developers" ] + spec.email = [ "developers@hedgeye.com" ] + spec.homepage = "https://github.com/hedgeyedev/phlex_preview" + spec.summary = "A storybook implementation for Phlex components." + spec.description = "Showcase your Phlex components: rendering, documentation, and copyable code." + spec.license = "MIT" + + spec.post_install_message = <<~MESSAGE + PhlexStorybook requires additional setup. + Please run the following command to install the necessary files: + thor phlex_storybook:install + MESSAGE + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = spec.homepage + spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" + spec.metadata["rubygems_mfa_required"] = "true" + + spec.files = Dir.chdir(File.expand_path(__dir__)) do + Dir["{app,config,lib}/**/*", "LICENSE", "Rakefile", "README.md", "CHANGELOG.md"] + end + + spec.add_dependency "importmap-rails" + spec.add_dependency "phlex-rails", "~> 1.2" + spec.add_dependency "rails", ">= 7.2" + spec.add_dependency "stimulus-rails" + spec.add_dependency "tailwindcss-rails", "~> 2.7" + spec.add_dependency "turbo-rails" + spec.add_dependency "turbo_power", "~> 0.6.2" + spec.add_dependency "phlex-icons", "~> 0.11.0" + spec.add_dependency "rouge", "~> 4.3.0" +end diff --git a/rendered_results_preview_component.rb b/rendered_results_preview_component.rb deleted file mode 100644 index 55a5c21..0000000 --- a/rendered_results_preview_component.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'rouge' -class RenderedResultsPreviewComponent < ApplicationComponent - def initialize(html) - @html = html - @formatter = Rouge::Formatters::HTML.new(css_class: 'highlight') - @lexer = Rouge::Lexers::HTML.new - end - - def view_template - common_styles - style do - <<-CSS - .rendered-results-preview { - margin: 20px; - padding: 10px; - border: 1px solid #ccc; - border-radius: 5px; - } - .raw-html { - white-space: pre-wrap; /* Maintains spacing and format */ - background-color: #f4f4f4; - border: 1px dashed #ddd; - padding: 10px; - overflow-x: auto; - } - .highlight { - background-color: #ffffcc; - } - CSS - end - style do - Rouge::Themes::ThankfulEyes.render(scope: '.highlight').gsub("\n", "") - end - div(class: 'rendered-results-preview') do - h1 { 'Rendered Results Preview' } - hr - unsafe_raw @html - hr - end - div(class: 'rendered-results-preview raw-html') do - h1 { 'Raw HTML' } - div(class: 'highlight') { unsafe_raw @formatter.format(@lexer.lex(@html)) } - end - end - -end - -# Sample invocation: -# RenderedResultsPreviewComponent.new("

text") -# End Sample invocation 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..e06b931 --- /dev/null +++ b/test/dummy/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css +//= link phlex_storybook_manifest.js 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/another_dummy_component.rb b/test/dummy/app/components/another_dummy_component.rb new file mode 100644 index 0000000..a19fb8d --- /dev/null +++ b/test/dummy/app/components/another_dummy_component.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +class AnotherDummyComponent < Phlex::HTML + def self.component_category + "Category 2" + end + + def self.component_description + <<~TEXT + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Morbi hendrerit libero euismod nisl venenatis sollicitudin. + Curabitur in leo vel justo vulputate ornare at id dolor. + Curabitur at diam quis orci efficitur ullamcorper. + Nullam posuere mauris eget elit blandit dapibus sit amet id purus. + Phasellus eleifend ante sit amet lacus tristique, sed sodales nisl porttitor. + Nullam mollis nisi non magna tincidunt, quis ullamcorper odio suscipit. + Sed pretium ligula ut mi ullamcorper eleifend. + Pellentesque eget metus a lorem feugiat feugiat eget sed nunc. + Aenean blandit urna non tortor gravida viverra. + Aenean quis nunc pulvinar, lacinia metus non, lacinia lorem. + Nullam nec magna id ante venenatis gravida. + Mauris vestibulum metus ut erat feugiat, sit amet cursus lectus commodo. + Donec elementum purus consectetur leo pharetra finibus. + Duis viverra odio eget posuere varius. + Integer tristique odio ac diam porttitor, id maximus lacus ultricies. + Donec semper neque eget leo gravida malesuada. + Ut non tortor nec ipsum euismod dignissim in suscipit leo. + Sed imperdiet risus sit amet mi rutrum luctus. + TEXT + end + + def self.component_props + [ + PhlexStorybook::Props::String.new(key: :header, placeholder: "The header", required: true), + PhlexStorybook::Props::Text.new(key: :text, label: "The list"), + PhlexStorybook::Props::Boolean.new(key: :truthy, label: "Truthy"), + PhlexStorybook::Props::Select.new(key: :selectable, label: "Select", include_blank: true, options: %w[Option1 Option2 Option3]), + PhlexStorybook::Props::Select.new(key: :multi_selectable, label: "Select Several", multiple: true, options: %w[Option1 Option2 Option3]), + ] + end + + def self.component_stories + [ + { title: "Short List", header: "Candidates", text: short_string, truthy: true }, + { title: "Still Deciding", header: "Things", text: long_string, truthy: false }, + ] + end + + def self.long_string + 100.times.map { |i| "list item #{i + 1}" }.join("\n") + end + + def self.short_string + 3.times.map { |i| "Candidate #{i + 1}" }.join("\n") + end + + def initialize(header:, text: "", truthy: false, selectable: [], multi_selectable: []) + @text = text + @header = header + @multi_selectable = multi_selectable + @selectable = selectable + @truthy = truthy + end + + def view_template + h1 { @header } + pre(class: "border border-slate-700 p-2") { @text } + div { @truthy ? "This is true" : "This is false" } + div { @selectable } + ul do + @multi_selectable.each do |option| + li { option } + end + end + end +end diff --git a/test/dummy/app/components/dummy_component.rb b/test/dummy/app/components/dummy_component.rb new file mode 100644 index 0000000..62b0abd --- /dev/null +++ b/test/dummy/app/components/dummy_component.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class DummyComponent < Phlex::HTML + def self.component_category + "Category 1" + end + + def self.component_description + "This is a dummy component" + end + + def self.component_name + "Dummy Component" + end + + def view_template + h1 { "The Title" } + ul do + %w[Story1 Story2 Story3].each do |story| + li { story } + end + end + end +end diff --git a/test/dummy/app/components/rendered_results_preview_component.rb b/test/dummy/app/components/rendered_results_preview_component.rb new file mode 100644 index 0000000..3364371 --- /dev/null +++ b/test/dummy/app/components/rendered_results_preview_component.rb @@ -0,0 +1,110 @@ +class RenderedResultsPreviewComponent < Phlex::HTML + + def self.component_props + [ + PhlexStorybook::Props::Text.new(key: :html, placeholder: "HTML", required: true), + ] + end + + def self.component_stories + [ + { title: "Short Doc", html: short_html }, + { title: "Long Doc", html: long_html }, + ] + end + + def self.short_html + <<~HTML + + + + Sample HTML + + +

Sample HTML

+

This is a sample HTML document.

+ + + HTML + end + + def self.long_html + list_items = 50.times.map { |i| "
  • list item #{i + 1}
  • " } + <<~HTML + + + + Sample HTML + + + +

    Sample HTML

    +
      + #{list_items.first.strip}\n#{list_items[1..-1].join("\n")} +
    + + + HTML + end + + def initialize(html:) + @html = html + @formatter = Rouge::Formatters::HTML.new(css_class: 'highlight') + @lexer = Rouge::Lexers::HTML.new + end + + def view_template + style do + <<~CSS.squish + .rendered-results-preview, .raw-html { + margin: 20px; + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #fff; + overflow-x: auto; + } + .highlight { + background-color: #f4f4f4; + border: 1px dashed #ddd; + font-family: Monaco, Consolas, monospace; + font-size: 14px; + white-space: pre-wrap; + } + .raw-html { + white-space: pre-wrap; /* Maintains spacing and format */ + background-color: #f4f4f4; + border: 1px dashed #ddd; + font-family: Monaco, Consolas, monospace; + } + .highlight { + background-color: #ffffcc; + } + CSS + end + style do + Rouge::Themes::ThankfulEyes.render(scope: '.highlight').gsub("\n", "") + end + div(class: 'rendered-results-preview') do + h1 { 'Rendered Results Preview' } + hr + unsafe_raw @html + hr + end + div(class: 'rendered-results-preview raw-html') do + h1(style: "font-family: Karla, sans-serif") { 'Raw HTML' } + div(class: 'highlight') { unsafe_raw @formatter.format(@lexer.lex(@html)) } + end + end + +end diff --git a/test/dummy/app/components/storyless_component.rb b/test/dummy/app/components/storyless_component.rb new file mode 100644 index 0000000..d22675e --- /dev/null +++ b/test/dummy/app/components/storyless_component.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class StorylessComponent < Phlex::HTML + def self.component_category + "Category 1" + end + + def self.component_description + "This is a storyless component" + end + + def self.component_name + "Component without stories" + end + + def self.component_props + [ + PhlexStorybook::Props::String.new(key: :header, default: "Default Header", required: true), + PhlexStorybook::Props::Text.new(key: :text, label: "The list"), + ] + end + + def initialize(header:, text: "ant\nbat\ncat\ndog") + @text = text || "" + @header = header + end + + def view_template + h1 { @header } + pre { @text } + end +end diff --git a/test/dummy/app/constraints/admin_constraint.rb b/test/dummy/app/constraints/admin_constraint.rb new file mode 100644 index 0000000..8667cd1 --- /dev/null +++ b/test/dummy/app/constraints/admin_constraint.rb @@ -0,0 +1,5 @@ +class AdminConstraint + def self.matches?(request) + true + 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/concerns/.keep b/test/dummy/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 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/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..3c34c81 --- /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..b63caeb --- /dev/null +++ b/test/dummy/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + primary_abstract_class +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/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb new file mode 100644 index 0000000..f72b4ef --- /dev/null +++ b/test/dummy/app/views/layouts/application.html.erb @@ -0,0 +1,15 @@ + + + + Dummy + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "application" %> + + + + <%= 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..3aac900 --- /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/bin/rails b/test/dummy/bin/rails new file mode 100755 index 0000000..efc0377 --- /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..3cd5a9d --- /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, exception: true) +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts "== Installing dependencies ==" + system! "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..a2fc1be --- /dev/null +++ b/test/dummy/config/application.rb @@ -0,0 +1,45 @@ +require_relative "boot" + +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +# require "active_job/railtie" +# require "active_record/railtie" +# require "active_storage/engine" +require "action_controller/railtie" +# require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" +require "action_view/railtie" +require "action_cable/engine" +require "rails/test_unit/railtie" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Dummy + class Application < Rails::Application + config.autoload_paths << "#{root}/app/views" + config.autoload_paths << "#{root}/app/views/layouts" + config.autoload_paths << "#{root}/app/views/components" + config.load_defaults Rails::VERSION::STRING.to_f + + # For compatibility with applications that use this config + config.action_controller.include_all_helpers = false + + # Please, add to the `ignore` list any other `lib` subdirectories that do + # not contain `.rb` files, or that should not be reloaded or eager loaded. + # Common ones are `templates`, `generators`, or `middleware`, for example. + config.autoload_lib(ignore: %w(assets tasks)) + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + config.time_zone = "Eastern Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + config.hosts = nil + end +end diff --git a/test/dummy/config/boot.rb b/test/dummy/config/boot.rb new file mode 100644 index 0000000..116591a --- /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/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..b3dc0ba --- /dev/null +++ b/test/dummy/config/environments/development.rb @@ -0,0 +1,62 @@ +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.enable_reloading = true + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable server timing + config.server_timing = true + + # Enable/disable 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 + + # 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 = [] + + # 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 + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/test/dummy/config/environments/production.rb b/test/dummy/config/environments/production.rb new file mode 100644 index 0000000..3eaecd1 --- /dev/null +++ b/test/dummy/config/environments/production.rb @@ -0,0 +1,97 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.enable_reloading = false + + # Eager load code on boot. 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 ENV["RAILS_MASTER_KEY"], config/master.key, or an environment + # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. + # config.public_file_server.enabled = false + + # Compress CSS using a preprocessor. + # config.assets.css_compressor = :sass + + # Do not fall back 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.*/ ] + + # Assume all access to the app is happening through a SSL-terminating reverse proxy. + # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. + # config.assume_ssl = true + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true + + # Log to STDOUT by default + config.logger = ActiveSupport::Logger.new(STDOUT) + .tap { |logger| logger.formatter = ::Logger::Formatter.new } + .then { |logger| ActiveSupport::TaggedLogging.new(logger) } + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # "info" includes generic and useful information about system operation, but avoids logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). If you + # want to log everything, set the level to "debug". + config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") + + # 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 + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Do not dump schema after migrations. + # config.active_record.dump_schema_after_migration = false + + # Enable DNS rebinding protection and other `Host` header attacks. + # config.hosts = [ + # "example.com", # Allow requests from example.com + # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` + # ] + # Skip DNS rebinding protection for the default health check endpoint. + # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } +end diff --git a/test/dummy/config/environments/test.rb b/test/dummy/config/environments/test.rb new file mode 100644 index 0000000..1686d38 --- /dev/null +++ b/test/dummy/config/environments/test.rb @@ -0,0 +1,57 @@ +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. + + # While tests run files are not watched, reloading is not necessary. + config.enable_reloading = false + + # Eager loading loads your entire application. When running a single test locally, + # this is usually not necessary, and can slow down your test suite. However, it's + # recommended that you enable it in continuous integration systems to ensure eager + # loading is working properly before deploying your code. + config.eager_load = ENV["CI"].present? + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.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 + + # Render exception templates for rescuable exceptions and raise for other exceptions. + config.action_dispatch.show_exceptions = :rescuable + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory. + # config.active_storage.service = :test + + # 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 + + # Raise error when a before_action's only/except options reference missing actions + config.action_controller.raise_on_missing_callback_actions = true +end diff --git a/test/dummy/config/initializers/assets.rb b/test/dummy/config/initializers/assets.rb new file mode 100644 index 0000000..2eeef96 --- /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/content_security_policy.rb b/test/dummy/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..b3076b3 --- /dev/null +++ b/test/dummy/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy. +# See the Securing Rails Applications Guide for more information: +# https://guides.rubyonrails.org/security.html#content-security-policy-header + +# Rails.application.configure do +# config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end +# +# # Generate session nonces for permitted importmap, inline scripts, and inline styles. +# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } +# config.content_security_policy_nonce_directives = %w(script-src style-src) +# +# # Report violations without enforcing the policy. +# # config.content_security_policy_report_only = true +# end diff --git a/test/dummy/config/initializers/filter_parameter_logging.rb b/test/dummy/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..c010b83 --- /dev/null +++ b/test/dummy/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. +# Use this to limit dissemination of sensitive information. +# See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. +Rails.application.config.filter_parameters += [ + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +] diff --git a/test/dummy/config/initializers/inflections.rb b/test/dummy/config/initializers/inflections.rb new file mode 100644 index 0000000..3860f65 --- /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/permissions_policy.rb b/test/dummy/config/initializers/permissions_policy.rb new file mode 100644 index 0000000..7db3b95 --- /dev/null +++ b/test/dummy/config/initializers/permissions_policy.rb @@ -0,0 +1,13 @@ +# Be sure to restart your server when you modify this file. + +# 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 |policy| +# policy.camera :none +# policy.gyroscope :none +# policy.microphone :none +# policy.usb :none +# policy.fullscreen :self +# policy.payment :self, "https://secure.example.com" +# end diff --git a/test/dummy/config/initializers/phlex_storybook.rb b/test/dummy/config/initializers/phlex_storybook.rb new file mode 100644 index 0000000..017eb48 --- /dev/null +++ b/test/dummy/config/initializers/phlex_storybook.rb @@ -0,0 +1,2 @@ +PhlexStorybook.configure do |config| +end diff --git a/test/dummy/config/locales/en.yml b/test/dummy/config/locales/en.yml new file mode 100644 index 0000000..6c349ae --- /dev/null +++ b/test/dummy/config/locales/en.yml @@ -0,0 +1,31 @@ +# Files in the config/locales directory are used for internationalization and +# are automatically loaded by Rails. If you want to use locales other than +# English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t "hello" +# +# In views, this is aliased to just `t`: +# +# <%= t("hello") %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more about the API, please read the Rails Internationalization guide +# at https://guides.rubyonrails.org/i18n.html. +# +# Be aware that YAML interprets the following case-insensitive strings as +# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings +# must be quoted to be interpreted as strings. For example: +# +# en: +# "yes": yup +# enabled: "ON" + +en: + hello: "Hello world" diff --git a/test/dummy/config/puma.rb b/test/dummy/config/puma.rb new file mode 100644 index 0000000..3dc0e60 --- /dev/null +++ b/test/dummy/config/puma.rb @@ -0,0 +1,37 @@ +# This configuration file will be evaluated by Puma. The top-level methods that +# are invoked here are part of Puma's configuration DSL. For more information +# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. + +# Puma starts a configurable number of processes (workers) and each process +# serves each request in a thread from an internal thread pool. +# +# The ideal number of threads per worker depends both on how much time the +# application spends waiting for IO operations and on how much you wish to +# to prioritize throughput over latency. +# +# As a rule of thumb, increasing the number of threads will increase how much +# traffic a given process can handle (throughput), but due to CRuby's +# Global VM Lock (GVL) it has diminishing returns and will degrade the +# response time (latency) of the application. +# +# The default is set to 3 threads as it's deemed a decent compromise between +# throughput and latency for the average Rails application. +# +# Any libraries that use a connection pool or another resource pool should +# be configured to provide at least as many connections as the number of +# threads. This includes Active Record's `pool` parameter in `database.yml`. +threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +port ENV.fetch("PORT", 3001) + +# Allow puma to be restarted by `bin/rails restart` command. +plugin :tmp_restart + +# Puma can rebuild our tailwindcss stylesheets in development +# plugin :tailwindcss if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specify the PID file. Defaults to tmp/pids/server.pid in development. +# In other environments, only set the PID file if requested. +pidfile ENV["PIDFILE"] if ENV["PIDFILE"] diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb new file mode 100644 index 0000000..69d815c --- /dev/null +++ b/test/dummy/config/routes.rb @@ -0,0 +1,5 @@ +Rails.application.routes.draw do + mount PhlexStorybook::Engine => "/phlex_storybook" + + root to: "phlex_storybook/stories#index" +end diff --git a/test/dummy/config/storage.yml b/test/dummy/config/storage.yml new file mode 100644 index 0000000..4942ab6 --- /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 bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket-<%= Rails.env %> + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket-<%= Rails.env %> + +# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name-<%= Rails.env %> + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/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