diff --git a/.template/addons/bootstrap/application.scss.rb b/.template/addons/bootstrap/application.scss.rb index ac4043f9a..7b45c4bfb 100644 --- a/.template/addons/bootstrap/application.scss.rb +++ b/.template/addons/bootstrap/application.scss.rb @@ -2,6 +2,6 @@ insert_into_file 'app/assets/stylesheets/application.scss', after: %r{// Dependencies\n} do <<~SCSS - @import './vendor'; + @import 'vendor'; SCSS end diff --git a/.template/addons/bootstrap/stylesheets/vendor/index.scss b/.template/addons/bootstrap/stylesheets/vendor/index.scss index bdef18b0a..f9030ae8d 100644 --- a/.template/addons/bootstrap/stylesheets/vendor/index.scss +++ b/.template/addons/bootstrap/stylesheets/vendor/index.scss @@ -1 +1 @@ -@import './bootstrap'; +@import 'bootstrap'; diff --git a/.template/addons/crud/app/assets/stylesheets/layouts/_application.scss b/.template/addons/crud/app/assets/stylesheets/layouts/_application.scss new file mode 100644 index 000000000..d7b40cc8d --- /dev/null +++ b/.template/addons/crud/app/assets/stylesheets/layouts/_application.scss @@ -0,0 +1,93 @@ +body { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.app { + display: flex; + flex-direction: row; + flex-grow: 1; + + &__container { + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: space-between; + } + + &__main { + padding: rem(16px); + margin: rem(16px); + } + + &__footer { + grid-area: footer; + padding: rem(8px); + } + + &__nav-bar-top { + grid-area: navBar; + border-bottom: solid var(--bs-border-width) var(--bs-gray-400); + } + + &__side-bar { + grid-area: sideBar; + display: flex; + flex-flow: column; + padding: rem(32px); + background-color: var(--bs-light); + width: rem(280px); + height: 100%; + + .dropdown-toggle { + outline: 0; + } + + .btn-toggle { + padding: 0.25rem 0.5rem; + font-weight: 600; + color: var(--bs-emphasis-color); + background-color: transparent; + + &:hover, + &:focus { + color: rgba(var(--bs-emphasis-color-rgb), 0.85); + background-color: var(--bs-tertiary-bg); + } + + &::before { + width: 1.25em; + line-height: 0; + content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); + transition: transform 0.35s ease; + transform-origin: 0.5em 50%; + } + } + + [data-bs-theme="dark"] .btn-toggle::before { + content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%28255,255,255,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); + } + + .btn-toggle[aria-expanded="true"] { + color: rgba(var(--bs-emphasis-color-rgb), 0.85); + } + .btn-toggle[aria-expanded="true"]::before { + transform: rotate(90deg); + } + + .btn-toggle-nav a { + padding: 0.1875rem 0.5rem; + margin-top: 0.125rem; + margin-left: 1.25rem; + } + .btn-toggle-nav a:hover, + .btn-toggle-nav a:focus { + background-color: var(--bs-tertiary-bg); + } + + .scrollarea { + overflow-y: auto; + } + } +} diff --git a/.template/addons/crud/app/assets/stylesheets/layouts/index.scss b/.template/addons/crud/app/assets/stylesheets/layouts/index.scss new file mode 100644 index 000000000..714cf9a78 --- /dev/null +++ b/.template/addons/crud/app/assets/stylesheets/layouts/index.scss @@ -0,0 +1 @@ +@import "application"; diff --git a/.template/addons/crud/app/views/layouts/application.html.slim.tt b/.template/addons/crud/app/views/layouts/application.html.slim.tt new file mode 100644 index 000000000..965d1ba7d --- /dev/null +++ b/.template/addons/crud/app/views/layouts/application.html.slim.tt @@ -0,0 +1,25 @@ +doctype html +html lang=I18n.locale + head + meta charset="utf-8" + meta name="viewport" content="width=device-width, initial-scale=1.0" + title= content_for?(:title) ? yield(:title) : '<%= app_name %>' + = csrf_meta_tags + = csp_meta_tag + = stylesheet_link_tag 'application', :media => 'all' + = javascript_include_tag 'application' + + body class=class_names(controller_name.dasherize, action_name.dasherize) + header + = render('layouts/application/nav_bar_top') + .app + .side-bar + = render('layouts/application/side_bar') + .app__container + main.app__main + = yield + footer.app__footer + p.text-center.text-muted + | © + = ' <%= app_name %>' + = Date.today.year diff --git a/.template/addons/crud/app/views/layouts/application/_nav_bar_top.html.slim b/.template/addons/crud/app/views/layouts/application/_nav_bar_top.html.slim new file mode 100644 index 000000000..ee5e68814 --- /dev/null +++ b/.template/addons/crud/app/views/layouts/application/_nav_bar_top.html.slim @@ -0,0 +1,33 @@ +.app__nav-bar-top + nav.navbar.navbar-expand-lg.navbar-light.bg-light + .container-fluid + a.navbar-brand[href="#"] + | NavbarTop + button.navbar-toggler[type="button" data-bs-toggle="collapse" data-bs-target="#top_navbar_nav" aria-controls="top_navbar_nav" aria-expanded="false" aria-label="Toggle navigation"] + span.navbar-toggler-icon + #top_navbar_nav.collapse.navbar-collapse + ul.navbar-nav.me-auto.mb-2.mb-lg-0 + li.nav-item + a.nav-link.active[aria-current="page" href="#"] + | Home + li.nav-item + a.nav-link[href="#"] + | Link + li.nav-item + a.nav-link.disabled[href="#" tabindex="-1" aria-disabled="true"] + | Disabled + li.nav-item.dropdown + a#dropdown09.nav-link.dropdown-toggle[href="#" data-bs-toggle="dropdown" aria-expanded="false"] + | Account + ul.dropdown-menu[aria-labelledby="dropdown09"] + li + a.dropdown-item[href="#"] + | Profile + li + a.dropdown-item[href="#"] + | Settings + li + a.dropdown-item[href="#"] + | Logout + form + input.form-control[type="text" placeholder="Search" aria-label="Search"] diff --git a/.template/addons/crud/app/views/layouts/application/_side_bar.html.slim b/.template/addons/crud/app/views/layouts/application/_side_bar.html.slim new file mode 100644 index 000000000..372812275 --- /dev/null +++ b/.template/addons/crud/app/views/layouts/application/_side_bar.html.slim @@ -0,0 +1,74 @@ +.app__side-bar + a.d-flex.align-items-center.mb-3.mb-md-0.me-md-auto.link-dark.text-decoration-none[href="/"] + svg.bi.pe-none.me-2[width="40" height="32"] + use[xlink:href="#bootstrap"] + span.fs-4 + | Sidebar + hr + ul.list-unstyled.ps-0 + li.mb-1 + button.btn.btn-toggle.d-inline-flex.align-items-center.rounded.border-0.collapsed[data-bs-toggle="collapse" data-bs-target="#home-collapse" aria-expanded="false"] + | Home + #home-collapse.collapse[style=""] + ul.btn-toggle-nav.list-unstyled.fw-normal.pb-1.small + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Overview + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Updates + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Reports + li.mb-1 + button.btn.btn-toggle.d-inline-flex.align-items-center.rounded.border-0.collapsed[data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false"] + | Dashboard + #dashboard-collapse.collapse[style=""] + ul.btn-toggle-nav.list-unstyled.fw-normal.pb-1.small + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Overview + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Weekly + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Monthly + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Annually + li.mb-1 + button.btn.btn-toggle.d-inline-flex.align-items-center.rounded.border-0.collapsed[data-bs-toggle="collapse" data-bs-target="#orders-collapse" aria-expanded="false"] + | Orders + #orders-collapse.collapse[style=""] + ul.btn-toggle-nav.list-unstyled.fw-normal.pb-1.small + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | New + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Processed + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Shipped + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Returned + li.border-top.my-3 + li.mb-1 + button.btn.btn-toggle.d-inline-flex.align-items-center.rounded.border-0.collapsed[data-bs-toggle="collapse" data-bs-target="#account-collapse" aria-expanded="false"] + | Account + #account-collapse.collapse + ul.btn-toggle-nav.list-unstyled.fw-normal.pb-1.small + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | New... + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Profile + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Settings + li + a.link-body-emphasis.d-inline-flex.text-decoration-none.rounded[href="#"] + | Sign out diff --git a/.template/addons/crud/lib/template.rb b/.template/addons/crud/lib/template.rb new file mode 100644 index 000000000..117cd98d3 --- /dev/null +++ b/.template/addons/crud/lib/template.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require File.expand_path('../../../lib/thor_utils', __dir__) + +use_source_path __dir__ + +def copy_template_files + ThorUtils.ignore_tt do + directory 'lib/templates', renderTemplates: false + end +end + +copy_template_files diff --git a/.template/addons/crud/lib/templates/slim/scaffold/index.html.slim.tt b/.template/addons/crud/lib/templates/slim/scaffold/index.html.slim.tt new file mode 100644 index 000000000..e28b31cc4 --- /dev/null +++ b/.template/addons/crud/lib/templates/slim/scaffold/index.html.slim.tt @@ -0,0 +1,21 @@ +h1 <%= human_name.pluralize %> + +#<%= plural_table_name %> + table.table.table-hover.table-responsive + thead + tr +<% attributes.each do |attribute| -%> + th <%= attribute.human_name %> +<% end -%> + th colspan="3" + tbody + - @<%= plural_table_name %>.each do |<%= singular_table_name %>| + tr +<% attributes.each do |attribute| -%> + td = <%= singular_table_name %>.<%= attribute.name %> +<% end -%> + td = link_to 'Show', <%= singular_table_name %>, class: 'btn btn-info' + td = link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>), class: 'btn btn-primary' + td = link_to 'Destroy', <%= singular_table_name %>, data: { turbo_method: :delete, confirm: 'Are you sure' }, class: 'btn btn-danger' + += link_to "New <%= human_name.downcase %>", <%= new_helper(type: :path) %>, class: 'btn btn-success' diff --git a/.template/addons/crud/template.rb b/.template/addons/crud/template.rb new file mode 100644 index 000000000..544ad2807 --- /dev/null +++ b/.template/addons/crud/template.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +use_source_path __dir__ + +remove_file 'app/views/layouts/application.html.erb' +directory 'app/views/layouts/application' +template 'app/views/layouts/application.html.slim.tt' + +# SCSS Layout +directory 'app/assets/stylesheets/layouts' +insert_into_file 'app/assets/stylesheets/application.scss', after: %r{// Layouts\n} do + <<~SCSS + @import 'layouts'; + SCSS +end + +apply 'lib/template.rb' diff --git a/.template/addons/slim/Gemfile.rb b/.template/addons/slim/Gemfile.rb index 879265d51..1d9ed2372 100644 --- a/.template/addons/slim/Gemfile.rb +++ b/.template/addons/slim/Gemfile.rb @@ -4,7 +4,7 @@ <<~RUBY # Templating - gem 'slim' # light weight template engine + gem 'slim-rails' # Slim generator for Rails RUBY end diff --git a/.template/lib/thor_utils.rb b/.template/lib/thor_utils.rb new file mode 100644 index 000000000..15fd52815 --- /dev/null +++ b/.template/lib/thor_utils.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class ThorUtils + def self.ignore_tt + # NOTE: change template extension so it would skip + # `when /#{TEMPLATE_EXTNAME}$/` condition and + # fallback to default `copy_file` + Thor::TEMPLATE_EXTNAME.concat '_no_match' # => .tt_no_match + yield + ensure + # NOTE: make sure to undo our dirty work after the block + Thor::TEMPLATE_EXTNAME.chomp! '_no_match' # => .tt + end +end diff --git a/.template/spec/addons/variants/web/bootstrap/application_scss_spec.rb b/.template/spec/addons/variants/web/bootstrap/application_scss_spec.rb index 8fcdeb59b..9414dd960 100644 --- a/.template/spec/addons/variants/web/bootstrap/application_scss_spec.rb +++ b/.template/spec/addons/variants/web/bootstrap/application_scss_spec.rb @@ -4,6 +4,6 @@ subject { file('app/assets/stylesheets/application.scss') } it 'imports vendor stylesheets' do - expect(subject).to contain("@import './vendor';") + expect(subject).to contain("@import 'vendor';") end end diff --git a/.template/spec/addons/variants/web/crud/application_layout_slim_spec.rb b/.template/spec/addons/variants/web/crud/application_layout_slim_spec.rb new file mode 100644 index 000000000..e4d46b618 --- /dev/null +++ b/.template/spec/addons/variants/web/crud/application_layout_slim_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +describe 'CRUD Addon - application layout' do + it 'attaches the current locale to the html tag' do + expect(file('app/views/layouts/application.html.slim')).to contain('html lang=I18n.locale') + end + + it 'loads the stylesheet entry file in the layout' do + expect(file('app/views/layouts/application.html.slim')).to contain("javascript_include_tag 'application'") + end + + it 'loads the javascript entry file in the layout' do + expect(file('app/views/layouts/application.html.slim')).to contain("stylesheet_link_tag 'application'") + end +end diff --git a/.template/spec/addons/variants/web/crud/application_scss_spec.rb b/.template/spec/addons/variants/web/crud/application_scss_spec.rb new file mode 100644 index 000000000..59395f6e8 --- /dev/null +++ b/.template/spec/addons/variants/web/crud/application_scss_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +describe 'CRUD Addon - application.scss' do + subject { file('app/assets/stylesheets/application.scss') } + + it 'imports layouts stylesheets' do + expect(subject).to contain("@import 'layouts';") + end +end diff --git a/.template/spec/addons/variants/web/crud/template_spec.rb b/.template/spec/addons/variants/web/crud/template_spec.rb new file mode 100644 index 000000000..1fd376469 --- /dev/null +++ b/.template/spec/addons/variants/web/crud/template_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +describe 'CRUD addon - template' do + it 'creates app/assets/stylesheets/layouts/index.scss' do + expect(file('app/assets/stylesheets/layouts/index.scss')).to exist + end + + it 'removes app/views/layouts/application.html.erb' do + expect(file('app/views/layouts/application.html.erb')).not_to exist + end + + it 'creates javascript/vendor/index.js' do + expect(file('app/javascript/vendor/index.js')).to exist + end + + it 'creates app/views/layouts/application.html.slim' do + expect(file('app/views/layouts/application.html.slim')).to exist + end + + it 'creates lib/templates/slim/scaffold/index.html.slim' do + expect(file('lib/templates/slim/scaffold/index.html.slim.tt')).to exist + end +end diff --git a/.template/spec/variants/web/app/template_spec.rb b/.template/spec/variants/web/app/template_spec.rb index d296d6dbc..e8402211c 100644 --- a/.template/spec/variants/web/app/template_spec.rb +++ b/.template/spec/variants/web/app/template_spec.rb @@ -21,10 +21,6 @@ expect(file('app/javascript/application.js')).to exist end - it 'loads the javascript entry file in the layout' do - expect(file('app/views/layouts/application.html.erb')).to contain('<%= javascript_include_tag "application"') - end - it 'imports necessary modules' do expect(file('app/javascript/application.js')).to contain('import \'./translations/translations\';') @@ -103,9 +99,5 @@ it 'includes the localization concern in the application controller' do expect(file('app/controllers/application_controller.rb')).to contain('include Localization') end - - it 'modifies the html tag to attach the current locale' do - expect(file('app/views/layouts/application.html.erb')).to contain("") - end end end diff --git a/.template/variants/web/app/assets/stylesheets/application.scss b/.template/variants/web/app/assets/stylesheets/application.scss index 1f0188d34..da75b3971 100644 --- a/.template/variants/web/app/assets/stylesheets/application.scss +++ b/.template/variants/web/app/assets/stylesheets/application.scss @@ -1,10 +1,10 @@ // Variables -@import './variables'; +@import 'variables'; // Dependencies // Functions -@import './functions'; +@import 'functions'; // Mixins diff --git a/.template/variants/web/app/assets/stylesheets/functions/index.scss b/.template/variants/web/app/assets/stylesheets/functions/index.scss index 9642f39c3..fccb41151 100644 --- a/.template/variants/web/app/assets/stylesheets/functions/index.scss +++ b/.template/variants/web/app/assets/stylesheets/functions/index.scss @@ -1 +1 @@ -@import './sizing'; +@import 'sizing'; diff --git a/.template/variants/web/app/template.rb b/.template/variants/web/app/template.rb index ab983770f..658d85749 100644 --- a/.template/variants/web/app/template.rb +++ b/.template/variants/web/app/template.rb @@ -3,13 +3,22 @@ # Javascript directory 'app/javascript' -if File.exist?('app/views/layouts/application.html.erb') - insert_into_file 'app/views/layouts/application.html.erb', before: %r{} do +erb_layout_file = 'app/views/layouts/application.html.erb' +slim_layout_file = 'app/views/layouts/application.html.slim' + +erb_layout_exists = File.exist?(erb_layout_file) +slim_layout_exists = File.exist?(slim_layout_file) +layout_not_found = !erb_layout_exists && !slim_layout_exists + +if erb_layout_exists + insert_into_file erb_layout_file, before: %r{} do <<~ERB.indent(2) <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> ERB end -else +end + +if layout_not_found @template_errors.add <<~ERROR Cannot include javascript into `app/views/layouts/application.html.erb` Content: <%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> @@ -33,11 +42,13 @@ RUBY end -if File.exist?('app/views/layouts/application.html.erb') - gsub_file 'app/views/layouts/application.html.erb', // do +if erb_layout_exists + gsub_file erb_layout_file, // do "" end -else +end + +if layout_not_found @template_errors.add <<~ERROR Cannot insert the lang attribute into html tag into `app/views/layouts/application.html.erb` Content: diff --git a/Makefile b/Makefile index 4535f9702..ac7f31d9e 100644 --- a/Makefile +++ b/Makefile @@ -4,14 +4,15 @@ # Y - in response to Would you like to add the SemaphoreCI addon? # Y - in response to Would you like to add the Nginx addon? # Y - in response to Would you like to add the Phrase addon? -# Y - in response to Would you like to add the Devise addon? -# Y - in response to Would you like to add the Bootstrap addon? -# Y - in response to Would you like to add the Slim Template Engine addon? +# Y - in response to Would you like to add the CRUD addon? +# - Devise auto included with the CRUD addon. +# - Bootstrap auto included with the CRUD addon. +# - Slim Template Engine auto included with the CRUD addon. # Y - in response to Would you like to add the Hotwire addon? # Y - in response to Would you like to add the Svgeez addon? create_web: - printf "Y\nY\nY\nY\nY\nY\nY\nY\nY\nY\nY\n" | rails new $(APP_NAME) -m ./template.rb -T ${OPTIONS} + printf "Y\nY\nY\nY\nY\nY\nY\nY\nY\n" | rails new $(APP_NAME) -m ./template.rb -T ${OPTIONS} # Y - in response to Would you like to add the Github addon? # Y - in response to Would you like to add the Mock Server addon? diff --git a/template.rb b/template.rb index 6a7b592bf..7660c1194 100644 --- a/template.rb +++ b/template.rb @@ -77,26 +77,35 @@ def apply_template!(template_root) # Custom Rubocop cops apply '.template/addons/custom_cops/template.rb' + apply '.template/addons/crud/template.rb' if @install_crud + # A list necessary jobs that run before complete, ex: Fixing rubocop on Ruby files that generated by Rails apply '.template/hooks/before_complete/fix_rubocop.rb' apply '.template/hooks/before_complete/report.rb' end def ask_for_optional_addons - @install_github_action = true if yes?(install_addon_prompt('Github Action and Wiki')) - @install_openapi = true if API_VARIANT || yes?(install_addon_prompt('OpenAPI')) - @install_mock_server = true if @install_openapi && yes?(install_addon_prompt('Mock Server')) - @install_semaphore = true if yes?(install_addon_prompt('SemaphoreCI')) - @install_nginx = true if yes?(install_addon_prompt('Nginx')) - @install_phrase = true if yes?(install_addon_prompt('Phrase')) - @install_devise = true if yes?(install_addon_prompt('Devise')) - - return unless WEB_VARIANT - - @install_bootstrap = true if yes?(install_addon_prompt('Bootstrap')) - @install_slim = true if yes?(install_addon_prompt('Slim Template Engine')) - @install_hotwire = true if yes?(install_addon_prompt('Hotwire')) - @install_svgeez = true if yes?(install_addon_prompt('Svgeez')) + ask_for_all_variant_addons + + ask_for_web_variant_addons if WEB_VARIANT +end + +def ask_for_all_variant_addons + @install_github_action = yes?(install_addon_prompt('Github Action and Wiki')) + @install_openapi = API_VARIANT || yes?(install_addon_prompt('OpenAPI')) + @install_mock_server = @install_openapi && yes?(install_addon_prompt('Mock Server')) + @install_semaphore = yes?(install_addon_prompt('SemaphoreCI')) + @install_nginx = yes?(install_addon_prompt('Nginx')) + @install_phrase = yes?(install_addon_prompt('Phrase')) + @install_crud = WEB_VARIANT && yes?(install_addon_prompt('Crud (includes Devise, Bootstrap and Slim)')) + @install_devise = @install_crud || yes?(install_addon_prompt('Devise')) +end + +def ask_for_web_variant_addons + @install_bootstrap = @install_crud || yes?(install_addon_prompt('Bootstrap')) + @install_slim = @install_crud || yes?(install_addon_prompt('Slim Template Engine')) + @install_hotwire = yes?(install_addon_prompt('Hotwire')) + @install_svgeez = yes?(install_addon_prompt('Svgeez')) end def apply_optional_addons