tramway 0.4.8 → 0.4.9.1

Sign up to get free protection for your applications and to get access to all the features.
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