From 753928130a202491624622529098649f1cb4af7a Mon Sep 17 00:00:00 2001 From: loqimean Date: Sun, 20 Feb 2022 18:04:23 +0200 Subject: [PATCH 1/4] feat(project|lib|test) add TailwinCSS 3 support --- README.md | 109 +++++++---- .../simple_form/install_generator.rb | 9 +- .../initializers/simple_form_tailwindcss.rb | 176 ++++++++++++++++++ test/generators/simple_form_generator_test.rb | 8 + 4 files changed, 266 insertions(+), 36 deletions(-) create mode 100644 lib/generators/simple_form/templates/config/initializers/simple_form_tailwindcss.rb diff --git a/README.md b/README.md index 792a63e67..966d0d20f 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,50 @@ You will need to provide your own CSS styles for hints. Please see the [instructions on how to install Foundation in a Rails app](http://foundation.zurb.com/docs/applications.html). +### TailwindCSS 3 + +To generate wrappers that are compatible with [TailwindCSS 3](https://tailwindcss.com/), pass +the `tailwindcss` option to the generator, like this: + +```console +rails generate simple_form:install --tailwindcss +``` + +Then add to your `tailwind.config.js` following config: + +``` +module.exports = { + content: [ + './config/initializers/simple_form_tailwindcss.rb' + ] +} +``` + +You have to be sure that you have a [tailwindcss-rails](https://github.com/rails/tailwindcss-rails) gem. + +**Warnings** + +Please pay attention that Tailwindcss not have owned a file-input style and with it, we can't modify `abbr` (star for required fields), so for styling, just add the next styles to your `app/assets/application.tailwind.css` or if you have a gem [tailwindcss_merger](https://github.com/loqimean/tailwindcss_merger) (for multiple files of tailwind's' additional styles) to `app/assets/tailwindcss_stylesheets/your_styles.css`: + +*For file-input:* + +``` +input[type=file]::-webkit-file-upload-button, +input[type=file]::file-selector-button { + @apply text-white bg-indigo-600 hover:bg-indigo-700 font-medium text-sm cursor-pointer border-0 py-2.5 pl-8 pr-4; + margin-inline-start: -1rem; + margin-inline-end: 1rem; +} +``` + +*For abbr of required fields:* + +``` +abbr[title=required] { + @apply !text-red-500 no-underline; +} +``` + ### Country Select If you want to use the country select, you will need the @@ -309,7 +353,6 @@ Collection inputs accept two other options beside collections: * *label_method* => the label method to be applied to the collection to retrieve the label (use this instead of the `text_method` option in `collection_select`) - * *value_method* => the value method to be applied to the collection to retrieve the value Those methods are useful to manipulate the given collection. Both of these options also accept @@ -345,7 +388,6 @@ used to retrieve label/value attributes for the `option` tags. Besides that, you * *group_method* => the method to be called on the given collection to generate the options for each group (required) - * *group_label_method* => the label method to be applied on the given collection to retrieve the label for the _optgroup_ (**Simple Form** will attempt to guess the best one the same way it does with `:label_method`) @@ -546,36 +588,36 @@ The following table shows the html element you will get for each attribute according to its database definition. These defaults can be changed by specifying the helper method in the column `Mapping` as the `as:` option. -Mapping | Generated HTML Element | Database Column Type ---------------- |--------------------------------------|--------------------- -`boolean` | `input[type=checkbox]` | `boolean` -`string` | `input[type=text]` | `string` -`citext` | `input[type=text]` | `citext` -`email` | `input[type=email]` | `string` with `name =~ /email/` -`url` | `input[type=url]` | `string` with `name =~ /url/` -`tel` | `input[type=tel]` | `string` with `name =~ /phone/` -`password` | `input[type=password]` | `string` with `name =~ /password/` -`search` | `input[type=search]` | - -`uuid` | `input[type=text]` | `uuid` -`color` | `input[type=color]` | `string` -`text` | `textarea` | `text` -`hstore` | `textarea` | `hstore` -`json` | `textarea` | `json` -`jsonb` | `textarea` | `jsonb` -`file` | `input[type=file]` | `string` responding to file methods -`hidden` | `input[type=hidden]` | - -`integer` | `input[type=number]` | `integer` -`float` | `input[type=number]` | `float` -`decimal` | `input[type=number]` | `decimal` -`range` | `input[type=range]` | - -`datetime` | `datetime select` | `datetime/timestamp` -`date` | `date select` | `date` -`time` | `time select` | `time` -`select` | `select` | `belongs_to`/`has_many`/`has_and_belongs_to_many` associations -`radio_buttons` | collection of `input[type=radio]` | `belongs_to` associations -`check_boxes` | collection of `input[type=checkbox]` | `has_many`/`has_and_belongs_to_many` associations -`country` | `select` (countries as options) | `string` with `name =~ /country/` -`time_zone` | `select` (timezones as options) | `string` with `name =~ /time_zone/` +| Mapping | Generated HTML Element | Database Column Type | +| ----------------- | -------------------------------------- | -------------------------------------------------------------------- | +| `boolean` | `input[type=checkbox]` | `boolean` | +| `string` | `input[type=text]` | `string` | +| `citext` | `input[type=text]` | `citext` | +| `email` | `input[type=email]` | `string` with `name =~ /email/` | +| `url` | `input[type=url]` | `string` with `name =~ /url/` | +| `tel` | `input[type=tel]` | `string` with `name =~ /phone/` | +| `password` | `input[type=password]` | `string` with `name =~ /password/` | +| `search` | `input[type=search]` | - | +| `uuid` | `input[type=text]` | `uuid` | +| `color` | `input[type=color]` | `string` | +| `text` | `textarea` | `text` | +| `hstore` | `textarea` | `hstore` | +| `json` | `textarea` | `json` | +| `jsonb` | `textarea` | `jsonb` | +| `file` | `input[type=file]` | `string` responding to file methods | +| `hidden` | `input[type=hidden]` | - | +| `integer` | `input[type=number]` | `integer` | +| `float` | `input[type=number]` | `float` | +| `decimal` | `input[type=number]` | `decimal` | +| `range` | `input[type=range]` | - | +| `datetime` | `datetime select` | `datetime/timestamp` | +| `date` | `date select` | `date` | +| `time` | `time select` | `time` | +| `select` | `select` | `belongs_to`/`has_many`/`has_and_belongs_to_many` associations | +| `radio_buttons` | collection of `input[type=radio]` | `belongs_to` associations | +| `check_boxes` | collection of `input[type=checkbox]` | `has_many`/`has_and_belongs_to_many` associations | +| `country` | `select` (countries as options) | `string` with `name =~ /country/` | +| `time_zone` | `select` (timezones as options) | `string` with `name =~ /time_zone/` | ## Custom inputs @@ -598,6 +640,7 @@ And use it in your views: ```ruby f.input :money, as: :currency ``` + Note, you may have to create the `app/inputs/` directory and restart your webserver. You can also redefine existing **Simple Form** inputs by creating a new class with the same name. For @@ -1034,7 +1077,7 @@ A cleaner method to create your views would be: To use the number option on the input, first, tells to Simple Form the place where the components will be: -``` ruby +```ruby # config/initializers/simple_form.rb Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } ``` diff --git a/lib/generators/simple_form/install_generator.rb b/lib/generators/simple_form/install_generator.rb index 34abd12d5..b20ce5935 100644 --- a/lib/generators/simple_form/install_generator.rb +++ b/lib/generators/simple_form/install_generator.rb @@ -7,12 +7,13 @@ class InstallGenerator < Rails::Generators::Base class_option :template_engine, desc: 'Template engine to be invoked (erb, haml or slim).' class_option :bootstrap, type: :boolean, desc: 'Add the Bootstrap wrappers to the SimpleForm initializer.' class_option :foundation, type: :boolean, desc: 'Add the Zurb Foundation 5 wrappers to the SimpleForm initializer.' + class_option :tailwindcss, type: :boolean, desc: 'Add the TailwindCSS 3 wrappers to the SimpleForm initializer.' def info_bootstrap - return if options.bootstrap? || options.foundation? - puts "SimpleForm 3 supports Bootstrap and Zurb Foundation 5. If you want "\ + return if options.bootstrap? || options.foundation? || options.tailwindcss? + puts "SimpleForm 3 supports Bootstrap, Zurb Foundation 5 and TailwindCSS 3. If you want "\ "a configuration that is compatible with one of these frameworks, then please " \ - "re-run this generator with --bootstrap or --foundation as an option." + "re-run this generator with --bootstrap, --foundation or --tailwindcss as an option." end def copy_config @@ -22,6 +23,8 @@ def copy_config template "config/initializers/simple_form_bootstrap.rb" elsif options[:foundation] template "config/initializers/simple_form_foundation.rb" + elsif options[:tailwindcss] + template "config/initializers/simple_form_tailwindcss.rb" end directory 'config/locales' diff --git a/lib/generators/simple_form/templates/config/initializers/simple_form_tailwindcss.rb b/lib/generators/simple_form/templates/config/initializers/simple_form_tailwindcss.rb new file mode 100644 index 000000000..50c1797e9 --- /dev/null +++ b/lib/generators/simple_form/templates/config/initializers/simple_form_tailwindcss.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +# Please do not make direct changes to this file! +# The generator is written by author named 'loqimean' +# All future development, tests, and organization should happen there. +# Background history: https://github.com/heartcombo/simple_form/issues/1561 + +# Uncomment this and change the path if necessary to include your own +# components. +# See https://github.com/heartcombo/simple_form#custom-components +# to know more about custom components. +# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f } + +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + config.wrappers :vertical_form, tag: 'div', class: 'mb-3', error_class: 'form-group-invalid', + valid_class: 'form-group-valid' do |b| + b.use :html5 + b.use :placeholder, class: 'text-gray-400' + b.optional :maxlength + b.optional :minlength + b.optional :pattern + b.optional :min_max + b.optional :readonly + b.use :label, class: 'ml-1 mb-2' + b.use :input, + class: 'mt-1 focus:ring-indigo-500 focus:border-indigo-500 ' \ + 'block w-full shadow-sm sm:text-sm border-gray-300 rounded-md', + error_class: 'border-red-400 is-invalid mb-1', valid_class: 'is-valid' + b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' } + b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' } + end + + # vertical input for radio buttons and check boxes + config.wrappers :vertical_collection, item_wrapper_class: 'form-check', + item_label_class: 'form-check-label', + tag: 'fieldset', + class: 'form-group mb-3', + error_class: 'form-group-invalid', + valid_class: 'form-group-valid' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| + ba.use :label_text, class: 'ml-3 block text-sm font-medium text-gray-700' + end + b.use :input, class: 'focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 mr-2', + error_class: 'is-invalid border-red-400', + valid_class: 'is-valid' + b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' } + b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' } + end + + # vertical input for radio buttons and check boxes + # config.wrappers :vertical_check_boxes_collection, item_wrapper_class: 'form-check', + # item_label_class: 'form-check-label', + # tag: 'fieldset', class: 'form-group mb-3', + # error_class: 'form-group-invalid', + # valid_class: 'form-group-valid' do |b| + # b.use :html5 + # b.optional :readonly + # b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba| + # ba.use :label_text, class: 'ml-3 block text-sm font-medium text-gray-700' + # end + # b.use :input, class: 'focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded mr-2', + # error_class: 'is-invalid border-red-400', + # valid_class: 'is-valid' + # b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' } + # b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' } + # end + + # horizontal input for inline radio buttons and check boxes + config.wrappers :horizontal_collection_inline, item_wrapper_class: 'form-check form-check-inline', + item_label_class: 'form-check-label', + tag: 'div', + class: 'form-group flex flex-row-reverse w-fit mb-3', + error_class: 'form-group-invalid', + valid_class: 'form-group-valid' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'pt-0 mb-2' + b.wrapper :grid_wrapper, tag: 'div' do |ba| + ba.use :input, class: 'focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded mr-2', + error_class: 'is-invalid', valid_class: 'is-valid' + ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' } + ba.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' } + end + end + + # vertical multi select + config.wrappers :vertical_multi_select, tag: 'div', class: 'mb-3', + error_class: 'form-group-invalid', + valid_class: 'form-group-valid' do |b| + b.use :html5 + b.optional :readonly + b.use :label, class: 'mb-2' + b.wrapper tag: 'div', class: 'flex flex-col md:flex-row gap-1 justify-between items-center' do |ba| + ba.use :input, class: 'w-full min-w-fit mt-1 focus:ring-indigo-500 focus:border-indigo-500 block shadow-sm sm:text-sm border-gray-300 rounded-md', + error_class: '!border-red-500', + valid_class: 'is-valid' + end + b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' } + b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' } + end + + # vertical input for boolean + config.wrappers :vertical_boolean, tag: 'fieldset', class: 'mb-3', + error_class: 'form-group-invalid', + valid_class: 'form-group-valid' do |b| + b.use :html5 + b.optional :readonly + b.wrapper :form_check_wrapper, tag: 'div' do |bb| + bb.use :input, class: 'focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded mr-2', + error_class: '!border-red-500', + valid_class: 'is-valid' + bb.use :label, class: 'mb-2' + end + b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' } + b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' } + end + + # vertical file input + config.wrappers :vertical_file, tag: 'div', + class: 'form-group', + error_class: 'form-group-invalid', + valid_class: 'form-group-valid' do |b| + b.use :html5 + b.use :placeholder + b.optional :maxlength + b.optional :minlength + b.optional :readonly + b.use :label, class: 'mb-2' + b.use :input, class: 'w-full min-w-fit mt-1 focus:ring-indigo-500 focus:border-indigo-500 block shadow-sm sm:text-sm border-gray-300 rounded-md border', + error_class: 'border-red-400 is-invalid mb-1', + valid_class: 'is-valid' + b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' } + b.use :hint, wrap_with: { tag: 'small', class: 'text-gray-400' } + end + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. + config.wrapper_mappings = { + boolean: :vertical_boolean, + check_boxes: :vertical_collection, + date: :vertical_multi_select, + file: :vertical_file, + datetime: :vertical_multi_select, + radio_buttons: :vertical_collection, + time: :vertical_multi_select + } + + # How the label text should be generated altogether with the required text. + config.label_text = ->(label, required, _explicit_label) { "#{label} #{required}" } + + # CSS class for buttons + config.button_class = 'rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer mb-1' + + # Set this to div to make the checkbox and radio properly work + # otherwise simple_form adds a label tag instead of a div around + # the nested label + config.item_wrapper_tag = :div + + # CSS class to add for error notification helper. + config.error_notification_class = 'bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded' + + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # :to_sentence to list all errors for each field. + config.error_method = :to_sentence + + # The default wrapper to be used by the FormBuilder. + config.default_wrapper = :vertical_form + + # add validation classes to `input_field` + config.input_field_error_class = 'is-invalid' + config.input_field_valid_class = 'is-valid' +end diff --git a/test/generators/simple_form_generator_test.rb b/test/generators/simple_form_generator_test.rb index abb526316..f15e2291f 100644 --- a/test/generators/simple_form_generator_test.rb +++ b/test/generators/simple_form_generator_test.rb @@ -34,6 +34,14 @@ class SimpleFormGeneratorTest < Rails::Generators::TestCase /config\.default_wrapper = :vertical_form/, /config\.item_wrapper_tag = :div/ end + test 'generates the simple_form initializer with the tailwindcss wrappers' do + run_generator %w[--tailwindcss] + assert_file 'config/initializers/simple_form.rb', + /config\.default_wrapper = :default/, /config\.boolean_style = :nested/ + assert_file 'config/initializers/simple_form_tailwindcss.rb', /config\.wrappers :vertical_form/, + /config\.default_wrapper = :vertical_form/, /config\.item_wrapper_tag = :div/ + end + %w[erb haml slim].each do |engine| test "generates the scaffold template when using #{engine}" do run_generator ['-e', engine] From fe620eb5383ba23cefe411502d53d3478aa47737 Mon Sep 17 00:00:00 2001 From: Ivan Marynych <49816584+loqimean@users.noreply.github.com> Date: Sun, 20 Feb 2022 23:11:41 +0200 Subject: [PATCH 2/4] return back changes --- README.md | 60 +++++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 966d0d20f..ed6a397f2 100644 --- a/README.md +++ b/README.md @@ -588,36 +588,36 @@ The following table shows the html element you will get for each attribute according to its database definition. These defaults can be changed by specifying the helper method in the column `Mapping` as the `as:` option. -| Mapping | Generated HTML Element | Database Column Type | -| ----------------- | -------------------------------------- | -------------------------------------------------------------------- | -| `boolean` | `input[type=checkbox]` | `boolean` | -| `string` | `input[type=text]` | `string` | -| `citext` | `input[type=text]` | `citext` | -| `email` | `input[type=email]` | `string` with `name =~ /email/` | -| `url` | `input[type=url]` | `string` with `name =~ /url/` | -| `tel` | `input[type=tel]` | `string` with `name =~ /phone/` | -| `password` | `input[type=password]` | `string` with `name =~ /password/` | -| `search` | `input[type=search]` | - | -| `uuid` | `input[type=text]` | `uuid` | -| `color` | `input[type=color]` | `string` | -| `text` | `textarea` | `text` | -| `hstore` | `textarea` | `hstore` | -| `json` | `textarea` | `json` | -| `jsonb` | `textarea` | `jsonb` | -| `file` | `input[type=file]` | `string` responding to file methods | -| `hidden` | `input[type=hidden]` | - | -| `integer` | `input[type=number]` | `integer` | -| `float` | `input[type=number]` | `float` | -| `decimal` | `input[type=number]` | `decimal` | -| `range` | `input[type=range]` | - | -| `datetime` | `datetime select` | `datetime/timestamp` | -| `date` | `date select` | `date` | -| `time` | `time select` | `time` | -| `select` | `select` | `belongs_to`/`has_many`/`has_and_belongs_to_many` associations | -| `radio_buttons` | collection of `input[type=radio]` | `belongs_to` associations | -| `check_boxes` | collection of `input[type=checkbox]` | `has_many`/`has_and_belongs_to_many` associations | -| `country` | `select` (countries as options) | `string` with `name =~ /country/` | -| `time_zone` | `select` (timezones as options) | `string` with `name =~ /time_zone/` | +Mapping | Generated HTML Element | Database Column Type +--------------- |--------------------------------------|--------------------- +`boolean` | `input[type=checkbox]` | `boolean` +`string` | `input[type=text]` | `string` +`citext` | `input[type=text]` | `citext` +`email` | `input[type=email]` | `string` with `name =~ /email/` +`url` | `input[type=url]` | `string` with `name =~ /url/` +`tel` | `input[type=tel]` | `string` with `name =~ /phone/` +`password` | `input[type=password]` | `string` with `name =~ /password/` +`search` | `input[type=search]` | - +`uuid` | `input[type=text]` | `uuid` +`color` | `input[type=color]` | `string` +`text` | `textarea` | `text` +`hstore` | `textarea` | `hstore` +`json` | `textarea` | `json` +`jsonb` | `textarea` | `jsonb` +`file` | `input[type=file]` | `string` responding to file methods +`hidden` | `input[type=hidden]` | - +`integer` | `input[type=number]` | `integer` +`float` | `input[type=number]` | `float` +`decimal` | `input[type=number]` | `decimal` +`range` | `input[type=range]` | - +`datetime` | `datetime select` | `datetime/timestamp` +`date` | `date select` | `date` +`time` | `time select` | `time` +`select` | `select` | `belongs_to`/`has_many`/`has_and_belongs_to_many` associations +`radio_buttons` | collection of `input[type=radio]` | `belongs_to` associations +`check_boxes` | collection of `input[type=checkbox]` | `has_many`/`has_and_belongs_to_many` associations +`country` | `select` (countries as options) | `string` with `name =~ /country/` +`time_zone` | `select` (timezones as options) | `string` with `name =~ /time_zone/` ## Custom inputs From 0641503244fbf3da4ee3bd091a03d9ac410af465 Mon Sep 17 00:00:00 2001 From: Ivan Marynych <49816584+loqimean@users.noreply.github.com> Date: Wed, 15 Jun 2022 14:07:47 +0300 Subject: [PATCH 3/4] feat(lib/simple_form/tags.rb) add data attribute option for item wrapper --- lib/simple_form/tags.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/simple_form/tags.rb b/lib/simple_form/tags.rb index 2d779b0dd..13c26e992 100644 --- a/lib/simple_form/tags.rb +++ b/lib/simple_form/tags.rb @@ -7,6 +7,7 @@ module CollectionExtensions def render_collection item_wrapper_tag = @options.fetch(:item_wrapper_tag, :span) item_wrapper_class = @options[:item_wrapper_class] + item_wrapper_data = @options[:item_wrapper_data] @collection.map do |item| value = value_for_collection(item, @value_method) @@ -22,7 +23,7 @@ def render_collection rendered_item = @template_object.label(@object_name, sanitize_attribute_name(value), rendered_item, label_options) end - item_wrapper_tag ? @template_object.content_tag(item_wrapper_tag, rendered_item, class: item_wrapper_class) : rendered_item + item_wrapper_tag ? @template_object.content_tag(item_wrapper_tag, rendered_item, class: item_wrapper_class, data: item_wrapper_data) : rendered_item end.join.html_safe end From cad1bb9637a8eb83326684e2e02c50b4559e2a4f Mon Sep 17 00:00:00 2001 From: loqimean Date: Mon, 19 Aug 2024 07:11:46 +0300 Subject: [PATCH 4/4] feat: add missing space in the bottom of file input --- .../templates/config/initializers/simple_form_tailwindcss.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/generators/simple_form/templates/config/initializers/simple_form_tailwindcss.rb b/lib/generators/simple_form/templates/config/initializers/simple_form_tailwindcss.rb index 50c1797e9..d0f643e9f 100644 --- a/lib/generators/simple_form/templates/config/initializers/simple_form_tailwindcss.rb +++ b/lib/generators/simple_form/templates/config/initializers/simple_form_tailwindcss.rb @@ -129,7 +129,7 @@ b.optional :minlength b.optional :readonly b.use :label, class: 'mb-2' - b.use :input, class: 'w-full min-w-fit mt-1 focus:ring-indigo-500 focus:border-indigo-500 block shadow-sm sm:text-sm border-gray-300 rounded-md border', + b.use :input, class: 'w-full min-w-fit mt-1 focus:ring-indigo-500 focus:border-indigo-500 block shadow-sm sm:text-sm border-gray-300 rounded-md border mb-3', error_class: 'border-red-400 is-invalid mb-1', valid_class: 'is-valid' b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback text-xs text-red-400' }