tramway 0.4.8 → 0.4.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e496f3ab6336d7f29b0b0021ef04b2bc18353ca3049406dd59671fbb8638d48f
4
- data.tar.gz: 7863120a81d6195f4e60bcae4d24a43845a6a184621bfe10ecc4dfcb8c83ff4c
3
+ metadata.gz: 588137c9cafa9b3b22de988ed0593b5ad34bda6480919b99b6eb7899cd8efb14
4
+ data.tar.gz: dbd1ecd2af4158892bb2e517cb3c962143f4f1109a00b8f392eaae392ad42205
5
5
  SHA512:
6
- metadata.gz: 374c6d983df5d172bf08e9f33fecb37abe192e4e6b8fd0a8863c3cd367f77206238abf05edfa7c0501919c860d90ee790fa16be721bc4c70adb2ad51245170fe
7
- data.tar.gz: 6876ad8aef86197b3b56c64cc07eddfcc9943aaa504d20b3d970d55e2fe197df16d6b5d28876d63cee75bca26e287fcc2ed078e3e420c5958607eac27fe78abe
6
+ metadata.gz: 8f3a4841ba2bdbe9e5bb1a12ab10900b90d43b916534af3f33eb57e6be0a33ae88f6148f7c795d584f7a9ac7cadc7b7bc38b411f8ca6694b0a7d23153295f7d8
7
+ data.tar.gz: 215ba73a338b8ab3d30b923696bb9cc22f189f9c8f1858c2e2a6cec0ce388e4fb4f30fee0345deaab19677416810f35935fa62b658b2c8f6b176e0d02da5a337
data/README.md CHANGED
@@ -8,7 +8,9 @@ Unite Ruby on Rails brilliance. Streamline development with Tramway.
8
8
  * [Tramway Form](https://github.com/Purple-Magic/tramway#tramway-form)
9
9
  * [Tramway Navbar](https://github.com/Purple-Magic/tramway#tramway-navbar)
10
10
  * [Tailwind-styled forms](https://github.com/Purple-Magic/tramway#tailwind-styled-forms)
11
+ * [Stimulus-based inputs](https://github.com/Purple-Magic/tramway#stimulus-based-inputs)
11
12
  * [Tailwind-styled pagination](https://github.com/Purple-Magic/tramway?tab=readme-ov-file#tailwind-styled-pagination-for-kaminari)
13
+ * [Articles](https://github.com/Purple-Magic/tramway#usage)
12
14
 
13
15
  ## Installation
14
16
  Add this line to your application's Gemfile:
@@ -421,10 +423,11 @@ Tramway uses [Tailwind](https://tailwindcss.com/) by default. All UI helpers are
421
423
  Tramway provides `tramway_form_for` helper that renders Tailwind-styled forms by default.
422
424
 
423
425
  ```ruby
424
- = tramway_form_for User.new do |f|
426
+ = tramway_form_for @user do |f|
425
427
  = f.text_field :text
426
428
  = f.password_field :password
427
429
  = f.select :role, [:admin, :user]
430
+ = f.multiselect :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']]
428
431
  = f.file_field :file
429
432
  = f.submit "Create User"
430
433
  ```
@@ -436,8 +439,42 @@ Available form helpers:
436
439
  * password_field
437
440
  * file_field
438
441
  * select
442
+ * multiselect ([Stimulus-based](https://github.com/Purple-Magic/tramway#stimulus-based-inputs))
439
443
  * submit
440
444
 
445
+ #### Stimulus-based inputs
446
+
447
+ `tramway_form_for` provides Tailwind-styled Stimulus-based custom inputs.
448
+
449
+ ##### Multiselect
450
+
451
+ In case you want to use tailwind-styled multiselect this way
452
+
453
+ ```haml
454
+ = tramway_form_for @user do |f|
455
+ = f.multiselect :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']]
456
+ #- ...
457
+ ```
458
+
459
+ you should add Tramway Multiselect Stimulus controller to your application.
460
+
461
+ Example for [importmap-rails](https://github.com/rails/importmap-rails) config
462
+
463
+ *config/importmap.rb*
464
+ ```ruby
465
+ pin '@tramway/multiselect', to: 'tramway/multiselect_controller.js'
466
+ ```
467
+
468
+ *app/javascript/controllers/index.js*
469
+ ```js
470
+ import { application } from "controllers/application"
471
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
472
+ import { Multiselect } from "@tramway/multiselect" // importing Multiselect controller class
473
+ eagerLoadControllersFrom("controllers", application)
474
+
475
+ application.register('multiselect', Multiselect) // register Multiselect controller class as `multiselect` stimulus controller
476
+ ```
477
+
441
478
  ### Tailwind-styled pagination for Kaminari
442
479
 
443
480
  Tramway uses [Tailwind](https://tailwindcss.com/) by default. It has tailwind-styled pagination for [kaminari](https://github.com/kaminari/kaminari).
@@ -480,6 +517,12 @@ user_2 = tramway_form User.first
480
517
  user_2.object #=> returns pure user object
481
518
  ```
482
519
 
520
+ ## Articles
521
+ * [Tramway on Rails](https://kalashnikovisme.medium.com/tramway-on-rails-32158c35ed68)
522
+ * [Delegating ActiveRecord methods to decorators in Rails](https://kalashnikovisme.medium.com/delegating-activerecord-methods-to-decorators-in-rails-4e4ec1c6b3a6)
523
+ * [Behave as ActiveRecord. Why do we want objects to be AR lookalikes?](https://kalashnikovisme.medium.com/behave-as-activerecord-why-do-we-want-objects-to-be-ar-lookalikes-d494d692e1d3)
524
+ * [Decorating associations in Rails with Tramway](https://kalashnikovisme.medium.com/decorating-associations-in-rails-with-tramway-b46a28392f9e)
525
+
483
526
  ## Contributing
484
527
 
485
528
  Install [lefthook](https://github.com/evilmartians/lefthook)
@@ -0,0 +1,131 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class Multiselect extends Controller {
4
+ static targets = ["dropdown", "showSelectedArea", "hiddenInput"];
5
+
6
+ static values = {
7
+ items: Array,
8
+ dropdownContainer: String,
9
+ itemContainer: String,
10
+ selectedItemTemplate: String,
11
+ dropdownState: String,
12
+ selectedItems: Array,
13
+ placeholder: String,
14
+ selectAsInput: String,
15
+ value: Array
16
+ }
17
+
18
+ connect() {
19
+ this.dropdownState = 'closed';
20
+ this.unselectedItems = JSON.parse(this.element.dataset.items).map((item) => {
21
+ return {
22
+ text: item.text,
23
+ value: item.value.toString()
24
+ }
25
+ });
26
+
27
+ const initialValues = this.element.dataset.value === undefined ? [] : this.element.dataset.value.split(',')
28
+ this.selectedItems = this.unselectedItems.filter(item => initialValues.includes(item.value));
29
+ this.unselectedItems = this.unselectedItems.filter(item => !initialValues.includes(item.value));
30
+
31
+ this.renderSelectedItems();
32
+ }
33
+
34
+ renderSelectedItems() {
35
+ const allItems = this.fillTemplate(this.element.dataset.selectedItemTemplate, this.selectedItems);
36
+ this.showSelectedAreaTarget.innerHTML = allItems;
37
+ this.showSelectedAreaTarget.insertAdjacentHTML("beforeEnd", this.input());
38
+ this.updateInputOptions();
39
+ }
40
+
41
+ fillTemplate(template, items) {
42
+ return items.map((item) => {
43
+ return template.replace(/{{text}}/g, item.text).replace(/{{value}}/g, item.value)
44
+ }).join('')
45
+ }
46
+
47
+ closeOnClickOutside(event) {
48
+ if (this.dropdownState === 'open' && !this.element.contains(event.target)) {
49
+ this.closeDropdown();
50
+ }
51
+ }
52
+
53
+ toggleDropdown() {
54
+ if (this.dropdownState === 'closed') {
55
+ this.openDropdown();
56
+ } else {
57
+ this.closeDropdown();
58
+ }
59
+ }
60
+
61
+ rerenderItems() {
62
+ this.closeDropdown();
63
+ this.openDropdown();
64
+ }
65
+
66
+ openDropdown() {
67
+ this.dropdownState = 'open';
68
+ this.dropdownTarget.insertAdjacentHTML("afterend", this.template);
69
+
70
+ if (this.dropdown()) {
71
+ this.dropdown().addEventListener('click', event => event.stopPropagation());
72
+ }
73
+ }
74
+
75
+ dropdown() {
76
+ return this.element.querySelector('#dropdown');
77
+ }
78
+
79
+ closeDropdown() {
80
+ this.dropdownState = 'closed';
81
+ if (this.dropdown()) {
82
+ this.dropdown().remove();
83
+ }
84
+ }
85
+
86
+ get template() {
87
+ return this.element.dataset.dropdownContainer.replace(
88
+ /{{content}}/g,
89
+ this.fillTemplate(this.element.dataset.itemContainer, this.unselectedItems)
90
+ );
91
+ }
92
+
93
+ toggleItem({ currentTarget }) {
94
+ const item = {
95
+ text: currentTarget.dataset.text,
96
+ value: currentTarget.dataset.value
97
+ };
98
+
99
+ const itemIndex = this.selectedItems.findIndex(x => x.value === item.value);
100
+ if (itemIndex !== -1) {
101
+ this.selectedItems = this.selectedItems.filter((_, index) => index !== itemIndex);
102
+ } else {
103
+ this.selectedItems.push(item);
104
+ }
105
+
106
+ this.unselectedItems = this.unselectedItems.filter(x => x.value !== item.value);
107
+
108
+ this.renderSelectedItems();
109
+ this.rerenderItems();
110
+ }
111
+
112
+ input() {
113
+ const placeholder = this.selectedItems.length > 0 ? '' : this.element.dataset.placeholder;
114
+ return this.element.dataset.selectAsInput.replace(/{{placeholder}}/g, placeholder);
115
+ }
116
+
117
+ updateInputOptions() {
118
+ this.hiddenInputTarget.innerHTML = '';
119
+ this.selectedItems.forEach(selected => {
120
+ const option = document.createElement("option");
121
+ option.text = selected.text;
122
+ option.value = selected.value;
123
+ option.setAttribute("selected", true);
124
+ this.hiddenInputTarget.append(option);
125
+ });
126
+
127
+ this.hiddenInputTarget.value = this.selectedItems.map(item => item.value);
128
+ }
129
+ }
130
+
131
+ export { Multiselect }
@@ -35,8 +35,17 @@ module Tailwinds
35
35
  ), &)
36
36
  end
37
37
 
38
- def submit(action, **options, &)
39
- render(Tailwinds::Form::SubmitButtonComponent.new(action, **options), &)
38
+ def multiselect(attribute, collection, **options, &)
39
+ render(Tailwinds::Form::MultiselectComponent.new(
40
+ input: input(:text_field),
41
+ value: options[:value] || options[:selected] || object.public_send(attribute)&.first,
42
+ collection:,
43
+ **default_options(attribute, options)
44
+ ), &)
45
+ end
46
+
47
+ def submit(action, **, &)
48
+ render(Tailwinds::Form::SubmitButtonComponent.new(action, **), &)
40
49
  end
41
50
 
42
51
  private
@@ -0,0 +1,3 @@
1
+ #dropdown.absolute.shadow.top-100.bg-white.z-40.w-full.lef-0.rounded.max-h-select.overflow-y-auto{ data: { action: "click@window->multiselect#closeOnClickOutside" } }
2
+ .flex.flex-col.w-full
3
+ {{content}}
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Form
5
+ module Multiselect
6
+ # Container for dropdown component
7
+ class DropdownContainer < ViewComponent::Base
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ .cursor-pointer.w-full.border-gray-100.rounded-t.border-b.hover:bg-teal-100{ data: { action: "click->multiselect#toggleItem", text: "{{text}}", value: "{{value}}" } }
2
+ .flex.w-full.items-center.p-2.pl-2.border-transparent.border-l-2.relative.hover:border-teal-100
3
+ .w-full.items-center.flex
4
+ .mx-2.leading-6
5
+ {{text}}
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Form
5
+ module Multiselect
6
+ # Container for item in dropdown component
7
+ class ItemContainer < ViewComponent::Base
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,2 @@
1
+ .flex-1
2
+ = @input.call(@attribute, @options.merge(placeholder: "{{placeholder}}", class: "bg-transparent p-1 px-2 appearance-none outline-none h-full w-full text-gray-800 hidden", data: { 'multiselect-target' => 'hiddenInput' }))
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Form
5
+ module Multiselect
6
+ # Renders input as select
7
+ class SelectAsInput < ViewComponent::Base
8
+ extend Dry::Initializer[undefined: false]
9
+
10
+ option :options
11
+ option :attribute
12
+ option :input
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ .flex.justify-center.items-center.m-1.font-medium.py-1.px-2.bg-white.rounded-full.text-teal-700.bg-teal-100.border.border-teal-300
2
+ .text-xs.font-normal.leading-none.max-w-full.flex-initial
3
+ {{text}}
4
+ .flex.flex-auto.flex-row-reverse
5
+ .cursor-pointer{ data: { action: "click->multiselect#toggleItem", text: "{{text}}", value: "{{value}}" } }
6
+
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Form
5
+ module Multiselect
6
+ # Tailwind-styled multi-select field
7
+ class SelectedItemTemplate < ViewComponent::Base
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ .mb-4
2
+ - if @label
3
+ %label.block.text-gray-700.text-sm.font-bold.mb-2{ for: @for }
4
+ = @label
5
+ .flex.flex-col.items-center.relative{ data: multiselect_hash, id: "#{@for}_multiselect" }
6
+ .min-w-96.w-fit
7
+ .p-1.flex.border.border-gray-200.bg-white.rounded{ data: { "multiselect-target" => "dropdown" } }
8
+ .flex.flex-auto.flex-wrap{ data: { "multiselect-target" => "showSelectedArea" } }
9
+ .text-gray-300.w-8.py-1.pl-2.pr-1.border-l.flex.items-center.border-gray-200
10
+ ^
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Form
5
+ # Tailwind-styled multi-select field
6
+ class MultiselectComponent < TailwindComponent
7
+ option :collection
8
+
9
+ def before_render
10
+ @collection = collection.map do |(text, value)|
11
+ { text:, value: }
12
+ end.to_json
13
+ end
14
+
15
+ def multiselect_hash
16
+ {
17
+ controller:, selected_item_template:, multiselect_selected_items_value:, dropdown_container:, item_container:,
18
+ items:, action:, select_as_input:, placeholder:, value:
19
+ }.transform_keys { |key| key.to_s.gsub('_', '-') }
20
+ end
21
+
22
+ private
23
+
24
+ def controller
25
+ :multiselect
26
+ end
27
+
28
+ def action
29
+ 'click->multiselect#toggleDropdown'
30
+ end
31
+
32
+ def items
33
+ collection
34
+ end
35
+
36
+ def placeholder
37
+ options[:placeholder]
38
+ end
39
+
40
+ def multiselect_selected_items_value
41
+ []
42
+ end
43
+
44
+ def select_as_input
45
+ render(Tailwinds::Form::Multiselect::SelectAsInput.new(options:, attribute:, input:))
46
+ end
47
+
48
+ def method_missing(method_name, *, &)
49
+ component = component_name(method_name)
50
+
51
+ if method_name.to_s.include?('_') && Object.const_defined?(component)
52
+ render(component.constantize.new(*, &))
53
+ else
54
+ super
55
+ end
56
+ end
57
+
58
+ def respond_to_missing?(method_name, include_private = false)
59
+ if method_name.to_s.include?('_') && Object.const_defined?(component_name(method_name))
60
+ true
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ # :reek:UtilityFunction { enabled: false }
67
+ def component_name(method_name)
68
+ "Tailwinds::Form::Multiselect::#{method_name.to_s.camelize}"
69
+ end
70
+ # :reek:UtilityFunction { enabled: true }
71
+ end
72
+ end
73
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Tailwinds
4
4
  module Form
5
- # Tailwind-styled text field
5
+ # Tailwind-styled select field
6
6
  class SelectComponent < TailwindComponent
7
7
  option :collection
8
8
  end
data/config/routes.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Lint/EmptyBlock
3
4
  Tramway::Engine.routes.draw do
4
5
  end
6
+ # rubocop:enable Lint/EmptyBlock
@@ -8,7 +8,7 @@ module Rules
8
8
  options.reduce({}) do |hash, (key, value)|
9
9
  case key
10
10
  when :method, :confirm
11
- hash.deep_merge data: { "turbo_#{key}".to_sym => value }
11
+ hash.deep_merge data: { "turbo_#{key}": value }
12
12
  else
13
13
  hash.merge key => value
14
14
  end
@@ -9,18 +9,25 @@ module Tramway
9
9
  attribute :name, Types::Coercible::String
10
10
  attribute? :route, Tramway::Configs::Entities::Route
11
11
 
12
+ # Route Struct contains implemented in Tramway CRUD and helpful routes for the entity
13
+ RouteStruct = Struct.new(:index)
14
+
15
+ # HumanNameStruct contains human names forms for the entity
16
+ HumanNameStruct = Struct.new(:single, :plural)
17
+
12
18
  def routes
13
- OpenStruct.new index: Rails.application.routes.url_helpers.public_send(route_helper_method)
19
+ RouteStruct.new(Rails.application.routes.url_helpers.public_send(route_helper_method))
14
20
  end
15
21
 
16
22
  def human_name
17
- options = if model_class.present?
18
- model_name = model_class.model_name.human
19
- { single: model_name, plural: model_name.pluralize }
20
- else
21
- { single: name.capitalize, plural: name.pluralize.capitalize }
22
- end
23
- OpenStruct.new(**options)
23
+ single, plural = if model_class.present?
24
+ model_name = model_class.model_name.human
25
+ [model_name, model_name.pluralize]
26
+ else
27
+ [name.capitalize, name.pluralize.capitalize]
28
+ end
29
+
30
+ HumanNameStruct.new(single, plural)
24
31
  end
25
32
 
26
33
  private
@@ -14,6 +14,10 @@ module Tramway
14
14
  configure_pagination if Tramway.config.pagination[:enabled]
15
15
  end
16
16
 
17
+ initializer 'tramway.assets.precompile' do |app|
18
+ app.config.assets.precompile += %w[tramway/multiselect.js]
19
+ end
20
+
17
21
  private
18
22
 
19
23
  def load_navbar_helper
@@ -27,7 +27,7 @@ module Tramway
27
27
  navbar_items.each do |(key, value)|
28
28
  key_to_merge = case key
29
29
  when :left, :right
30
- "#{key}_items".to_sym
30
+ :"#{key}_items"
31
31
  else
32
32
  key
33
33
  end
@@ -4,8 +4,8 @@ module Tramway
4
4
  module Helpers
5
5
  # Provides view-oriented helpers for ActionView
6
6
  module ViewsHelper
7
- def tramway_form_for(object, *args, **options, &)
8
- form_for(object, *args, **options.merge(builder: Tailwinds::Form::Builder), &)
7
+ def tramway_form_for(object, *, **options, &)
8
+ form_for(object, *, **options.merge(builder: Tailwinds::Form::Builder), &)
9
9
  end
10
10
  end
11
11
  end
@@ -33,13 +33,13 @@ module Tramway
33
33
  reset_filling
34
34
  end
35
35
 
36
- def item(text_or_url, url = nil, **options, &block)
37
- raise 'You cannot provide an argument and a code block at the same time' if provided_url_and_block?(url, &block)
36
+ def item(text_or_url, url = nil, **, &)
37
+ raise 'You cannot provide an argument and a code block at the same time' if provided_url_and_block?(url, &)
38
38
 
39
39
  rendered_item = if url.present?
40
- render_ignoring_block(text_or_url, url, **options)
40
+ render_ignoring_block(text_or_url, url, **)
41
41
  else
42
- render_using_block(text_or_url, **options, &block)
42
+ render_using_block(text_or_url, **, &)
43
43
  end
44
44
 
45
45
  @items[@filling] << rendered_item
@@ -79,13 +79,13 @@ module Tramway
79
79
  end
80
80
  end
81
81
 
82
- def render_using_block(text_or_url, method: nil, **options, &block)
82
+ def render_using_block(text_or_url, method: nil, **options, &)
83
83
  options.merge!(href: text_or_url)
84
84
 
85
85
  if method.present? && method.to_sym != :get
86
- context.render(Tailwinds::Nav::Item::ButtonComponent.new(method:, **options), &block)
86
+ context.render(Tailwinds::Nav::Item::ButtonComponent.new(method:, **options), &)
87
87
  else
88
- context.render(Tailwinds::Nav::Item::LinkComponent.new(method:, **options), &block)
88
+ context.render(Tailwinds::Nav::Item::LinkComponent.new(method:, **options), &)
89
89
  end
90
90
  end
91
91
  end
@@ -5,8 +5,8 @@ module Tramway
5
5
  # Provides helper method render that depends on ActionController::Base.render method
6
6
  #
7
7
  module Render
8
- def render(*args, &)
9
- ActionController::Base.render(*args, &)
8
+ def render(*, &)
9
+ ActionController::Base.render(*, &)
10
10
  end
11
11
  end
12
12
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tramway
4
- VERSION = '0.4.8'
4
+ VERSION = '0.4.9.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tramway
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.8
4
+ version: 0.4.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kalashnikovisme
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-08-08 00:00:00.000000000 Z
12
+ date: 2024-09-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: dry-struct
@@ -67,20 +67,6 @@ dependencies:
67
67
  - - ">="
68
68
  - !ruby/object:Gem::Version
69
69
  version: '0'
70
- - !ruby/object:Gem::Dependency
71
- name: rspec-rails
72
- requirement: !ruby/object:Gem::Requirement
73
- requirements:
74
- - - ">="
75
- - !ruby/object:Gem::Version
76
- version: '0'
77
- type: :development
78
- prerelease: false
79
- version_requirements: !ruby/object:Gem::Requirement
80
- requirements:
81
- - - ">="
82
- - !ruby/object:Gem::Version
83
- version: '0'
84
70
  description: Tramway Rails Engine
85
71
  email:
86
72
  - kalashnikovisme@gmail.com
@@ -91,11 +77,22 @@ files:
91
77
  - MIT-LICENSE
92
78
  - README.md
93
79
  - Rakefile
80
+ - app/assets/javascripts/tramway/multiselect_controller.js
94
81
  - app/components/tailwind_component.html.haml
95
82
  - app/components/tailwind_component.rb
96
83
  - app/components/tailwinds/form/builder.rb
97
84
  - app/components/tailwinds/form/file_field_component.html.haml
98
85
  - app/components/tailwinds/form/file_field_component.rb
86
+ - app/components/tailwinds/form/multiselect/dropdown_container.html.haml
87
+ - app/components/tailwinds/form/multiselect/dropdown_container.rb
88
+ - app/components/tailwinds/form/multiselect/item_container.html.haml
89
+ - app/components/tailwinds/form/multiselect/item_container.rb
90
+ - app/components/tailwinds/form/multiselect/select_as_input.html.haml
91
+ - app/components/tailwinds/form/multiselect/select_as_input.rb
92
+ - app/components/tailwinds/form/multiselect/selected_item_template.html.haml
93
+ - app/components/tailwinds/form/multiselect/selected_item_template.rb
94
+ - app/components/tailwinds/form/multiselect_component.html.haml
95
+ - app/components/tailwinds/form/multiselect_component.rb
99
96
  - app/components/tailwinds/form/select_component.html.haml
100
97
  - app/components/tailwinds/form/select_component.rb
101
98
  - app/components/tailwinds/form/submit_button_component.html.haml