tramway 0.5.4.1 → 0.5.5.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +42 -0
  3. data/app/components/tailwind_component.rb +32 -0
  4. data/app/components/tailwinds/form/builder.rb +53 -20
  5. data/app/components/tailwinds/form/file_field_component.html.haml +4 -1
  6. data/app/components/tailwinds/form/multiselect/dropdown_container.html.haml +1 -1
  7. data/app/components/tailwinds/form/multiselect/item_container.html.haml +3 -3
  8. data/app/components/tailwinds/form/multiselect/select_as_input.html.haml +4 -1
  9. data/app/components/tailwinds/form/multiselect/select_as_input.rb +1 -0
  10. data/app/components/tailwinds/form/multiselect/selected_item_template.html.haml +1 -1
  11. data/app/components/tailwinds/form/multiselect_component.html.haml +3 -3
  12. data/app/components/tailwinds/form/multiselect_component.rb +8 -1
  13. data/app/components/tailwinds/form/number_field_component.html.haml +8 -0
  14. data/app/components/tailwinds/form/number_field_component.rb +9 -0
  15. data/app/components/tailwinds/form/select_component.html.haml +5 -1
  16. data/app/components/tailwinds/form/submit_button_component.html.haml +4 -1
  17. data/app/components/tailwinds/form/submit_button_component.rb +11 -3
  18. data/app/components/tailwinds/form/text_area_component.html.haml +4 -1
  19. data/app/components/tailwinds/form/text_field_component.html.haml +4 -1
  20. data/app/components/tailwinds/table/header_component.html.haml +1 -1
  21. data/app/components/tailwinds/table/header_component.rb +6 -1
  22. data/app/components/tailwinds/table/row_component.rb +2 -6
  23. data/app/components/tailwinds/table_component.html.haml +1 -1
  24. data/app/controllers/tramway/entities_controller.rb +11 -5
  25. data/config/routes.rb +4 -4
  26. data/lib/tramway/base_form.rb +1 -1
  27. data/lib/tramway/decorators/association.rb +1 -1
  28. data/lib/tramway/decorators/class_helper.rb +18 -16
  29. data/lib/tramway/forms/properties.rb +6 -10
  30. data/lib/tramway/helpers/views_helper.rb +7 -2
  31. data/lib/tramway/navbar.rb +5 -5
  32. data/lib/tramway/version.rb +1 -1
  33. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b3a940800b0be153a5c1eb7655a1f4203ac977accf41579e43221766843c0e8
4
- data.tar.gz: ead462e416c64538748b393652233a4fb755f6c874dcf2d618e027ff4cfa77dc
3
+ metadata.gz: 7e2e7bd75d61196a4ba4302fe3c05d5dfce6928a35ea0d97022b9170ce911ca8
4
+ data.tar.gz: 1b89b1fb5a18ae6d28c8afb1b8a1d94da832f0855c1acf1f5ab4f9fb0f1d4bd7
5
5
  SHA512:
6
- metadata.gz: 835993aedeccd2cd7873c3cb12416d5c7c90f45d0cb234926e544a251e6bbc7a889a4a68db36f9adec55b360d246f146701a37307875ad7729f2f029f0e829fe
7
- data.tar.gz: b78685a91ce1adbb6da37659972a5206b891cccde030a3fb18ee8ba559f49bc73144b5db0636fedc22ed2f9466cab67e74afe0192f0c162283f1009e85ed4886
6
+ metadata.gz: 4565144363a3419833bb267231fc403ff49c1da50caa3ee4ec936d6a2245fc2ddf89ab111af33cc7a5759cd9bc51d90cc456e5108acc78b73e99ca7ea5107158
7
+ data.tar.gz: a83591d638a439dcd659474cfd50b73f9f292bff11fd2e18987c821df25888a9de4ae68d16fb2d7048eb36949869350339b8c3104ffaa71f92f97042d2a84ba9
data/README.md CHANGED
@@ -14,6 +14,16 @@ Unite Ruby on Rails brilliance. Streamline development with Tramway.
14
14
  * [Tailwind-styled pagination](https://github.com/Purple-Magic/tramway?tab=readme-ov-file#tailwind-styled-pagination-for-kaminari)
15
15
  * [Articles](https://github.com/Purple-Magic/tramway#usage)
16
16
 
17
+ ## Compatibility
18
+
19
+ Tramway is actively verified against the following Ruby and Rails versions.
20
+
21
+ | Ruby \ Rails | 7.1 | 7.2 | 8.0 | 8.1 |
22
+ | ------------- | --- | --- | --- | --- |
23
+ | 3.2 | ✅ | ✅ | ✅ | ✅ |
24
+ | 3.3 | ✅ | ✅ | ✅ | ✅ |
25
+ | 3.4 | ✅ | ✅ | ✅ | ✅ |
26
+
17
27
  ## Installation
18
28
  Add this line to your application's Gemfile:
19
29
 
@@ -528,6 +538,38 @@ Tramway provides a responsive, tailwind-styled table with light and dark themes.
528
538
  Another
529
539
  ```
530
540
 
541
+ `Tailwinds::TableComponent` accepts an optional `options` hash that is merged into the outer `.div-table` element. The hash is
542
+ forwarded as HTML attributes, so you can pass things like `id`, `data` attributes, or additional classes. If you do not supply
543
+ your own width utility (e.g. a class that starts with `w-`), the component automatically appends `w-full` to keep the table
544
+ responsive. This allows you to extend the default styling without losing the sensible defaults provided by the component.
545
+
546
+ ```haml
547
+ = component 'tailwinds/table', options: { class: 'max-w-3xl border border-gray-200', data: { controller: 'table' } } do
548
+ = component 'tailwinds/table/header', headers: ['Name', 'Email']
549
+ = component 'tailwinds/table/row' do
550
+ = component 'tailwinds/table/cell' do
551
+ = user.name
552
+ = component 'tailwinds/table/cell' do
553
+ = user.email
554
+ ```
555
+
556
+ When you render a header you can either pass the `headers:` array, as in the examples above, or render custom header content in
557
+ the block. `Tailwinds::Table::HeaderComponent` uses the length of the `headers` array to build the grid if the array is present.
558
+ If you omit the array and provide custom content, pass the `columns:` argument so the component knows how many grid columns to
559
+ generate.
560
+
561
+ ```haml
562
+ = component 'tailwinds/table/header', columns: 4 do
563
+ = component 'tailwinds/table/cell' do
564
+ Custom header cell
565
+ = component 'tailwinds/table/cell' do
566
+ Another header cell
567
+ / ...
568
+ ```
569
+
570
+ With this approach you control the header layout while still benefiting from the default Tailwind grid classes that the header
571
+ component applies.
572
+
531
573
  ### Tailwind-styled forms
532
574
 
533
575
  Tramway uses [Tailwind](https://tailwindcss.com/) by default. All UI helpers are implemented with [ViewComponent](https://github.com/viewcomponent/view_component).
@@ -10,4 +10,36 @@ class TailwindComponent < Tramway::Component::Base
10
10
  option :options
11
11
  option :label
12
12
  option :for
13
+ option :size, default: -> { :middle }
14
+
15
+ SIZE_CLASSES = {
16
+ small: {
17
+ text_input: 'text-sm px-2 py-1',
18
+ select_input: 'text-sm px-2 py-1',
19
+ file_button: 'text-sm px-3 py-1',
20
+ submit_button: 'text-sm px-3 py-1',
21
+ multiselect_input: 'text-sm px-2 py-1'
22
+ },
23
+ middle: {
24
+ text_input: 'text-base px-3 py-2',
25
+ select_input: 'text-base px-3 py-2',
26
+ file_button: 'text-base px-4 py-2',
27
+ submit_button: 'text-base px-4 py-2',
28
+ multiselect_input: 'text-base px-3 py-2'
29
+ },
30
+ large: {
31
+ text_input: 'text-lg px-4 py-3',
32
+ select_input: 'text-lg px-4 py-3',
33
+ file_button: 'text-lg px-5 py-3',
34
+ submit_button: 'text-lg px-5 py-3',
35
+ multiselect_input: 'text-lg px-4 py-3'
36
+ }
37
+ }.freeze
38
+
39
+ private
40
+
41
+ def size_class(key)
42
+ size_classes = SIZE_CLASSES.fetch(size) { SIZE_CLASSES[:middle] }
43
+ size_classes.fetch(key) { SIZE_CLASSES[:middle].fetch(key) }
44
+ end
13
45
  end
@@ -5,59 +5,90 @@ module Tailwinds
5
5
  # Provides Tailwind-styled forms
6
6
  # :reek:InstanceVariableAssumption
7
7
  class Builder < Tramway::Views::FormBuilder
8
+ def initialize(object_name, object, template, options)
9
+ super
10
+
11
+ @form_size = options[:size] || options['size'] || :middle
12
+ end
13
+
8
14
  def text_field(attribute, **options, &)
15
+ sanitized_options = sanitize_options(options)
16
+
9
17
  render(Tailwinds::Form::TextFieldComponent.new(
10
18
  input: input(:text_field),
11
- value: get_value(attribute, options),
12
- **default_options(attribute, options)
19
+ value: get_value(attribute, sanitized_options),
20
+ **default_options(attribute, sanitized_options)
21
+ ), &)
22
+ end
23
+
24
+ def number_field(attribute, **options, &)
25
+ sanitized_options = sanitize_options(options)
26
+
27
+ render(Tailwinds::Form::NumberFieldComponent.new(
28
+ input: input(:number_field),
29
+ value: get_value(attribute, sanitized_options),
30
+ **default_options(attribute, sanitized_options)
13
31
  ), &)
14
32
  end
15
33
 
16
34
  def text_area(attribute, **options, &)
35
+ sanitized_options = sanitize_options(options)
36
+
17
37
  render(Tailwinds::Form::TextAreaComponent.new(
18
38
  input: input(:text_area),
19
- value: get_value(attribute, options),
20
- **default_options(attribute, options)
39
+ value: get_value(attribute, sanitized_options),
40
+ **default_options(attribute, sanitized_options)
21
41
  ), &)
22
42
  end
23
43
 
24
44
  def password_field(attribute, **options, &)
45
+ sanitized_options = sanitize_options(options)
46
+
25
47
  render(Tailwinds::Form::TextFieldComponent.new(
26
48
  input: input(:password_field),
27
- **default_options(attribute, options)
49
+ **default_options(attribute, sanitized_options)
28
50
  ), &)
29
51
  end
30
52
 
31
53
  def file_field(attribute, **options, &)
32
- input = super(attribute, **options.merge(class: :hidden))
54
+ sanitized_options = sanitize_options(options)
55
+ input = super(attribute, **sanitized_options.merge(class: :hidden))
33
56
 
34
- render(Tailwinds::Form::FileFieldComponent.new(input:, **default_options(attribute, options)), &)
57
+ render(Tailwinds::Form::FileFieldComponent.new(input:, **default_options(attribute, sanitized_options)), &)
35
58
  end
36
59
 
37
60
  def select(attribute, collection, **options, &)
61
+ sanitized_options = sanitize_options(options)
62
+
38
63
  render(Tailwinds::Form::SelectComponent.new(
39
64
  input: input(:select),
40
- value: options[:selected] || object.public_send(attribute),
41
- collection: explicitly_add_blank_option(collection, options),
42
- **default_options(attribute, options)
65
+ value: sanitized_options[:selected] || object.public_send(attribute),
66
+ collection: explicitly_add_blank_option(collection, sanitized_options),
67
+ **default_options(attribute, sanitized_options)
43
68
  ), &)
44
69
  end
45
70
 
46
71
  def multiselect(attribute, collection, **options, &)
72
+ sanitized_options = sanitize_options(options)
73
+
47
74
  render(Tailwinds::Form::MultiselectComponent.new(
48
75
  input: input(:text_field),
49
- value: options[:value] || options[:selected] || object.public_send(attribute),
76
+ value: sanitized_options[:value] || sanitized_options[:selected] || object.public_send(attribute),
50
77
  collection:,
51
- **default_options(attribute, options)
78
+ **default_options(attribute, sanitized_options)
52
79
  ), &)
53
80
  end
54
81
 
55
- def submit(action, **, &)
56
- render(Tailwinds::Form::SubmitButtonComponent.new(action, **), &)
82
+ def submit(action, **options, &)
83
+ sanitized_options = sanitize_options(options)
84
+
85
+ render(Tailwinds::Form::SubmitButtonComponent.new(action, size: form_size, **sanitized_options), &)
57
86
  end
58
87
 
59
88
  private
60
89
 
90
+ attr_reader :form_size
91
+
61
92
  def input(method_name)
62
93
  unbound_method = self.class.superclass.instance_method(method_name)
63
94
  unbound_method.bind(self)
@@ -68,12 +99,7 @@ module Tailwinds
68
99
  end
69
100
 
70
101
  def default_options(attribute, options)
71
- {
72
- attribute:,
73
- label: label_build(attribute, options),
74
- for: for_id(attribute),
75
- options:
76
- }
102
+ { attribute:, label: label_build(attribute, options), for: for_id(attribute), options:, size: form_size }
77
103
  end
78
104
 
79
105
  # :reek:UtilityFunction
@@ -88,6 +114,13 @@ module Tailwinds
88
114
  "#{object_name}_#{attribute}"
89
115
  end
90
116
 
117
+ def sanitize_options(options)
118
+ options.dup.tap do |opts|
119
+ opts.delete(:size)
120
+ opts.delete('size')
121
+ end
122
+ end
123
+
91
124
  # REMOVE IT. WE MUST UNDERSTAND WHY INCLUDE_BLANK DOES NOT WORK
92
125
  # :reek:UtilityFunction
93
126
  def explicitly_add_blank_option(collection, options)
@@ -1,5 +1,8 @@
1
1
  .mb-4
2
2
  - if @label
3
- %label.inline-block.bg-blue-500.hover:bg-blue-700.text-white.font-bold.py-2.px-4.rounded.cursor-pointer.mt-4{ for: @for }
3
+ - base_classes = 'inline-block bg-blue-500 hover:bg-blue-700 text-white font-bold rounded cursor-pointer mt-4 '
4
+ - base_classes += 'dark:bg-blue-600 dark:hover:bg-blue-500'
5
+ - classes = "#{size_class(:file_button)} #{base_classes}"
6
+ %label{ for: @for, class: classes }
4
7
  = @label
5
8
  = @input
@@ -1,3 +1,3 @@
1
- #dropdown.absolute.shadow.top-11.bg-white.z-40.w-full.lef-0.rounded.max-h-select.overflow-y-auto{ data: { action: "click@window->multiselect#closeOnClickOutside" } }
1
+ #dropdown.absolute.shadow.top-11.bg-white.z-40.w-full.lef-0.rounded.max-h-select.overflow-y-auto.dark:bg-gray-800.dark:shadow-lg.dark:ring-1.dark:ring-gray-700{ data: { action: "click@window->multiselect#closeOnClickOutside" } }
2
2
  .flex.flex-col.w-full
3
3
  {{content}}
@@ -1,5 +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
1
+ .cursor-pointer.w-full.border-gray-100.rounded-t.border-b.hover:bg-teal-100.dark:border-gray-700.dark:hover:bg-gray-700{ 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.dark:hover:border-gray-600
3
+ .w-full.items-center.flex.dark:text-gray-100
4
4
  .mx-2.leading-6
5
5
  {{text}}
@@ -1,2 +1,5 @@
1
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' }))
2
+ - base_classes = 'bg-transparent appearance-none outline-none h-full w-full text-gray-800 hidden '
3
+ - base_classes += 'dark:text-white dark:placeholder-white'
4
+ - classes = "#{@size_class} #{base_classes}"
5
+ = @input.call(@attribute, @options.merge(placeholder: "{{placeholder}}", class: classes, data: { 'multiselect-target' => 'hiddenInput' }))
@@ -10,6 +10,7 @@ module Tailwinds
10
10
  option :options
11
11
  option :attribute
12
12
  option :input
13
+ option :size_class
13
14
  end
14
15
  end
15
16
  end
@@ -1,4 +1,4 @@
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
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.dark:bg-teal-900.dark:text-teal-100.dark:border-teal-700
2
2
  .text-xs.font-normal.leading-none.max-w-full.flex-initial
3
3
  {{text}}
4
4
  .flex.flex-auto.flex-row-reverse
@@ -2,9 +2,9 @@
2
2
  - if @label
3
3
  = component('tailwinds/form/label', for: @for) do
4
4
  = @label
5
- .flex.flex-col.relative{ data: multiselect_hash, id: "#{@for}_multiselect" }
5
+ .flex.flex-col.relative.dark:text-gray-100{ data: multiselect_hash, id: "#{@for}_multiselect" }
6
6
  .min-w-96.w-fit
7
- .p-1.flex.border.border-gray-200.bg-white.rounded{ data: { "multiselect-target" => "dropdown" } }
7
+ .p-1.flex.border.border-gray-200.bg-white.rounded.dark:border-gray-600.dark:bg-gray-800{ data: { "multiselect-target" => "dropdown" } }
8
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
9
+ .text-gray-300.w-8.py-1.pl-2.pr-1.border-l.flex.items-center.border-gray-200.dark:text-gray-500.dark:border-gray-600
10
10
  ^
@@ -45,7 +45,14 @@ module Tailwinds
45
45
  end
46
46
 
47
47
  def select_as_input
48
- render(Tailwinds::Form::Multiselect::SelectAsInput.new(options:, attribute:, input:))
48
+ render(
49
+ Tailwinds::Form::Multiselect::SelectAsInput.new(
50
+ options:,
51
+ attribute:,
52
+ input:,
53
+ size_class: size_class(:multiselect_input)
54
+ )
55
+ )
49
56
  end
50
57
 
51
58
  def on_change
@@ -0,0 +1,8 @@
1
+ .mb-4
2
+ - if @label
3
+ = component('tailwinds/form/label', for: @for) do
4
+ = @label
5
+ - base_classes = 'w-full bg-white border border-gray-300 rounded focus:outline-none focus:border-red-500 '
6
+ - base_classes += 'dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:border-red-400 dark:placeholder-white'
7
+ - classes = "#{size_class(:text_input)} #{base_classes}"
8
+ = @input.call @attribute, **@options.merge(class: classes), value: @value
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Form
5
+ # Tailwind-styled numeric field
6
+ class NumberFieldComponent < TailwindComponent
7
+ end
8
+ end
9
+ end
@@ -2,4 +2,8 @@
2
2
  - if @label
3
3
  = component('tailwinds/form/label', for: @for) do
4
4
  = @label
5
- = @input.call(@attribute, @collection, { selected: @value }, @options.merge(class: 'bg-white border border-gray-300 text-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent py-2 px-3 disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed'))
5
+ - base_classes = 'bg-white border border-gray-300 text-gray-700 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 '
6
+ - base_classes += 'focus:border-transparent disabled:bg-gray-100 disabled:text-gray-400 disabled:cursor-not-allowed '
7
+ - base_classes += 'dark:bg-gray-800 dark:border-gray-600 dark:text-gray-100 dark:focus:ring-red-400 dark:disabled:bg-gray-800 dark:disabled:text-gray-500'
8
+ - classes = "#{size_class(:select_input)} #{base_classes}"
9
+ = @input.call(@attribute, @collection, { selected: @value }, @options.merge(class: classes))
@@ -1,4 +1,7 @@
1
1
  .flex.items-center.justify-between
2
- %button.bg-red-500.hover:bg-red-700.text-white.font-bold.py-2.px-4.rounded.focus:outline-none.focus:shadow-outline.cursor-pointer.dark:text-white{ type: :submit, name: :commit, **@options }
2
+ - base_classes = 'bg-red-500 hover:bg-red-700 text-white font-bold rounded focus:outline-none focus:shadow-outline cursor-pointer '
3
+ - base_classes += 'dark:text-white dark:bg-red-600 dark:hover:bg-red-500 dark:focus:ring-2 dark:focus:ring-red-400'
4
+ - classes = "#{size_class(:submit_button)} #{base_classes}"
5
+ %button{ type: :submit, name: :commit, class: classes, **@options }
3
6
  = @text
4
7
  = @content
@@ -4,10 +4,18 @@ module Tailwinds
4
4
  module Form
5
5
  # Tailwind-styled submit button
6
6
  class SubmitButtonComponent < TailwindComponent
7
- def initialize(action, **options)
8
- @options = options.except :type
9
-
7
+ def initialize(action, size: :middle, **options)
10
8
  @text = action.is_a?(String) ? action : action.to_s.capitalize
9
+
10
+ super(
11
+ input: nil,
12
+ attribute: nil,
13
+ value: nil,
14
+ options: options.except(:type),
15
+ label: nil,
16
+ for: nil,
17
+ size:
18
+ )
11
19
  end
12
20
  end
13
21
  end
@@ -2,4 +2,7 @@
2
2
  - if @label
3
3
  = component('tailwinds/form/label', for: @for) do
4
4
  = @label
5
- = @input.call @attribute, **@options.merge(class: 'w-full bg-white px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-red-500 dark:placeholder-gray-400'), value: @value
5
+ - base_classes = 'w-full bg-white border border-gray-300 rounded focus:outline-none focus:border-red-500 '
6
+ - base_classes += 'dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:border-red-400 dark:placeholder-white'
7
+ - classes = "#{size_class(:text_input)} #{base_classes}"
8
+ = @input.call @attribute, **@options.merge(class: classes), value: @value
@@ -2,4 +2,7 @@
2
2
  - if @label
3
3
  = component('tailwinds/form/label', for: @for) do
4
4
  = @label
5
- = @input.call @attribute, **@options.merge(class: 'w-full bg-white px-3 py-2 border border-gray-300 rounded focus:outline-none focus:border-red-500 dark:placeholder-gray-400'), value: @value
5
+ - base_classes = 'w-full bg-white border border-gray-300 rounded focus:outline-none focus:border-red-500 '
6
+ - base_classes += 'dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:border-red-400 dark:placeholder-white'
7
+ - classes = "#{size_class(:text_input)} #{base_classes}"
8
+ = @input.call @attribute, **@options.merge(class: classes), value: @value
@@ -1,4 +1,4 @@
1
- .div-table-row.grid.text-white.text-small.gap-4.bg-purple-700.dark:bg-gray-700.dark:text-gray-400{ class: "grid-cols-#{headers.count}", aria: { label: "Table Header" }, role: "row" }
1
+ .div-table-row.grid.text-white.text-small.gap-4.bg-purple-700.dark:bg-gray-700.dark:text-gray-400{ class: "grid-cols-#{columns_count}", aria: { label: "Table Header" }, role: "row" }
2
2
  - if headers.any?
3
3
  - headers.each do |header|
4
4
  .div-table-cell.py-4.px-6
@@ -4,7 +4,12 @@ module Tailwinds
4
4
  module Table
5
5
  # Component for rendering a header in a table
6
6
  class HeaderComponent < Tramway::Component::Base
7
- option :headers
7
+ option :headers, optional: true, default: -> { [] }
8
+ option :columns, optional: true, default: -> { 3 }
9
+
10
+ def columns_count
11
+ headers.present? ? headers.size : columns
12
+ end
8
13
  end
9
14
  end
10
15
  end
@@ -12,26 +12,22 @@ module Tailwinds
12
12
  default_attributes = { role: :row }
13
13
 
14
14
  if href.present?
15
- klass = "#{options[:class] || ''} #{link_row_classes}"
16
-
17
- link_to(href, options.merge(class: klass, **default_attributes)) do
15
+ link_to(href, options.merge(class: "#{options[:class] || ''} #{link_row_classes}", **default_attributes)) do
18
16
  yield if block_given?
19
17
  end
20
18
  else
21
- tag.div(**options.merge(default_attributes)) do
19
+ tag.div(**options, **default_attributes) do
22
20
  yield if block_given?
23
21
  end
24
22
  end
25
23
  end
26
24
 
27
- # :reek:UtilityFunction { enabled: false }
28
25
  def desktop_row_classes(cells_count)
29
26
  [
30
27
  'div-table-row', 'grid', 'gap-4', 'bg-white', 'border-b', 'last:border-b-0', 'dark:bg-gray-800',
31
28
  'dark:border-gray-700', "grid-cols-#{cells_count}"
32
29
  ].join(' ')
33
30
  end
34
- # :reek:UtilityFunction { enabled: true }
35
31
 
36
32
  def link_row_classes
37
33
  'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700'
@@ -1,5 +1,5 @@
1
1
  = helpers.component 'tailwinds/table/row/preview'
2
2
 
3
3
  - width_class = options[:class]&.include?('w-') ? '' : 'w-full'
4
- .div-table.text-left.rtl:text-right.text-gray-500.dark:text-gray-400.mt-4{ class: "#{options[:class]} #{width_class}", **options.except(:class) }
4
+ .div-table.text-left.rtl:text-right.text-gray-500.dark:text-gray-400{ class: "#{options[:class]} #{width_class}", **options.except(:class) }
5
5
  = content
@@ -11,11 +11,13 @@ module Tramway
11
11
  include Rails.application.routes.url_helpers
12
12
 
13
13
  def index
14
- @entities = if entity.page(:index).scope.present?
15
- model_class.public_send(entity.page(:index).scope)
16
- else
17
- model_class.order(id: :desc)
18
- end.page(params[:page])
14
+ if index_scope.present?
15
+ model_class.public_send(index_scope)
16
+ else
17
+ model_class.order(id: :desc)
18
+ end.page(params[:page]) => entities
19
+
20
+ @entities = entities
19
21
 
20
22
  @namespace = entity.route&.namespace
21
23
  end
@@ -29,5 +31,9 @@ module Tramway
29
31
  def entity
30
32
  @entity ||= Tramway.config.entities.find { |e| e.name == params[:entity][:name] }
31
33
  end
34
+
35
+ def index_scope
36
+ entity.page(:index).scope
37
+ end
32
38
  end
33
39
  end
data/config/routes.rb CHANGED
@@ -3,13 +3,13 @@
3
3
  Tramway::Engine.routes.draw do
4
4
  Tramway.config.entities.each do |entity|
5
5
  segments = entity.name.split('/')
6
- resource_name = segments.pop
6
+ resource_name = segments.pop
7
7
 
8
8
  define_resource = proc do
9
9
  resources resource_name.pluralize.to_sym,
10
- only: [:index],
11
- controller:'/tramway/entities',
12
- defaults: { entity: entity }
10
+ only: [:index],
11
+ controller: '/tramway/entities',
12
+ defaults: { entity: entity }
13
13
  end
14
14
 
15
15
  if segments.empty?
@@ -50,7 +50,7 @@ module Tramway
50
50
  end
51
51
 
52
52
  def method_missing(method_name, *args)
53
- if method_name.to_s.end_with?('=') && args.count == 1
53
+ if method_name.to_s.end_with?('=') && args.one?
54
54
  object.public_send(method_name, args.first)
55
55
  else
56
56
  super
@@ -40,7 +40,7 @@ module Tramway
40
40
  class << self
41
41
  def decorate_has_many_association(assoc, decorator_class: nil)
42
42
  return [] if assoc.empty?
43
-
43
+
44
44
  decorator_class ||= decorator(assoc.klass)
45
45
 
46
46
  decorator_class.decorate(assoc)
@@ -20,26 +20,19 @@ module Tramway
20
20
  end
21
21
 
22
22
  def decorator_class_name(object_or_array_or_class, namespace)
23
- klass = if Tramway::Decorators::CollectionDecorators.collection?(object_or_array_or_class)
24
- object_or_array_or_class.first.class
25
- elsif object_or_array_or_class.is_a?(Class)
26
- object_or_array_or_class
27
- else
28
- object_or_array_or_class.class
29
- end
23
+ if Tramway::Decorators::CollectionDecorators.collection?(object_or_array_or_class)
24
+ object_or_array_or_class.first.class
25
+ elsif object_or_array_or_class.is_a?(Class)
26
+ object_or_array_or_class
27
+ else
28
+ object_or_array_or_class.class
29
+ end => klass
30
30
 
31
31
  base_class_name = Tramway::Decorators::NameBuilder.default_decorator_class_name(klass)
32
32
 
33
- klass_name = namespace.present? ? "#{namespace.to_s.camelize}::#{base_class_name}" : base_class_name
34
-
35
- if klass_name.safe_constantize
36
- klass_name
37
- else
38
- raise NameError, "You should define #{klass_name} decorator class in app/decorators/ folder."
39
- end
33
+ build_klass_name(base_class_name, namespace)
40
34
  end
41
35
 
42
- # :reek:NilCheck { enabled: false }
43
36
  def raise_error_if_object_empty(object_or_array, decorator)
44
37
  return unless object_or_array.blank? && decorator.nil?
45
38
 
@@ -47,7 +40,16 @@ module Tramway
47
40
 
48
41
  raise ArgumentError, text
49
42
  end
50
- # :reek:NilCheck { enabled: true }
43
+
44
+ def build_klass_name(base_class_name, namespace)
45
+ klass_name = namespace.present? ? "#{namespace.to_s.camelize}::#{base_class_name}" : base_class_name
46
+
47
+ unless klass_name.safe_constantize
48
+ raise NameError, "You should define #{klass_name} decorator class in app/decorators/ folder."
49
+ end
50
+
51
+ klass_name
52
+ end
51
53
  end
52
54
  end
53
55
  end
@@ -10,21 +10,17 @@ module Tramway
10
10
  @properties << attribute
11
11
 
12
12
  define_method(attribute) do
13
- if object.respond_to?(attribute)
14
- object.public_send(attribute)
15
- else
16
- raise NoMethodError, "#{self.class}##{attribute} is not defined"
17
- end
13
+ raise NoMethodError, "#{self.class}##{attribute} is not defined" unless object.respond_to?(attribute)
14
+
15
+ object.public_send(attribute)
18
16
  end
19
17
 
20
18
  set_method = "#{attribute}="
21
19
 
22
20
  define_method(set_method) do |value|
23
- if object.respond_to?(set_method)
24
- object.public_send(set_method, value)
25
- else
26
- raise NoMethodError, "#{self.class}##{set_method} is not defined"
27
- end
21
+ raise NoMethodError, "#{self.class}##{set_method} is not defined" unless object.respond_to?(set_method)
22
+
23
+ object.public_send(set_method, value)
28
24
  end
29
25
  end
30
26
 
@@ -4,8 +4,13 @@ module Tramway
4
4
  module Helpers
5
5
  # Provides view-oriented helpers for ActionView
6
6
  module ViewsHelper
7
- def tramway_form_for(object, *, **options, &)
8
- form_for(object, *, **options.merge(builder: Tailwinds::Form::Builder), &)
7
+ def tramway_form_for(object, *, size: :middle, **options, &)
8
+ form_for(
9
+ object,
10
+ *,
11
+ **options.merge(builder: Tailwinds::Form::Builder, size:),
12
+ &
13
+ )
9
14
  end
10
15
  end
11
16
  end
@@ -10,13 +10,13 @@ module Tramway
10
10
  @items = { left: [], right: [] }
11
11
  @filling = nil
12
12
 
13
- if with_entities
14
- entities = Tramway.config.entities
13
+ return unless with_entities
15
14
 
16
- return unless entities.any?
15
+ entities = Tramway.config.entities
17
16
 
18
- preset_left entities
19
- end
17
+ return unless entities.any?
18
+
19
+ preset_left entities
20
20
  end
21
21
 
22
22
  def left
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tramway
4
- VERSION = '0.5.4.1'
4
+ VERSION = '0.5.5.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.5.4.1
4
+ version: 0.5.5.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: 2025-07-11 00:00:00.000000000 Z
12
+ date: 2025-10-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: anyway_config
@@ -158,6 +158,8 @@ files:
158
158
  - app/components/tailwinds/form/multiselect/selected_item_template.rb
159
159
  - app/components/tailwinds/form/multiselect_component.html.haml
160
160
  - app/components/tailwinds/form/multiselect_component.rb
161
+ - app/components/tailwinds/form/number_field_component.html.haml
162
+ - app/components/tailwinds/form/number_field_component.rb
161
163
  - app/components/tailwinds/form/select_component.html.haml
162
164
  - app/components/tailwinds/form/select_component.rb
163
165
  - app/components/tailwinds/form/submit_button_component.html.haml