tramway 0.5.5 → 0.6

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: 21ffe76c2224bd46292479de272ccdb59f0095075043507af3dd6098d5559933
4
- data.tar.gz: 6547f3ac892439dddb165dbf7d8a06dcb90d8adb6f12b65d921cb277ebf7754a
3
+ metadata.gz: ff761d41cd4748bf3d7e1451825da24764f69077b842e92f7be6baf0f14dff55
4
+ data.tar.gz: d9c3b25519ab3eb4dfe9c7d2e26d8dffb339a2743b6c5c494495bd4c5b71fc6c
5
5
  SHA512:
6
- metadata.gz: 93bdfec725bc98cb5ab5a045604fe00c6669eaf8d89eedb081e0750a4b1073fda7c7f99964a3e4b8bbfb4f668024ca9341f512c5fed77d17ea05eeb31de19f37
7
- data.tar.gz: 6d6d161386768b114e82e031888ce118383bfb377c6b5e1551d9f8b7300f077af50eb6cb2bff3a6d4523ab0f43bbc77b69b3af180ed4895d7b8c24558c1876a4
6
+ metadata.gz: bb0c9c5121dc9a8213a0011c07265b2c42a486218966725df559c31f48b1f1e0cbbb50dc7fd10ddaac13f00a14d55ae57be42e121a1c1d44b1560822425f54af
7
+ data.tar.gz: 4488ab5046c8fee6150ee80459df3859f0a2d8b62948cfb91af93052dce2f0788aa067dde8261c9df0708ad31c853c522adc65c91c907cc71b4418c17bfc4244
data/README.md CHANGED
@@ -14,22 +14,33 @@ 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
 
20
30
  ```ruby
21
31
  gem "tramway"
22
- gem "haml-rails"
23
- gem "kaminari"
24
- gem "view_component"
25
32
  ```
26
33
 
27
- OR
34
+ Then install Tramway and its dependencies:
28
35
 
29
36
  ```shell
30
- bundle add tramway view_component kaminari view_component
37
+ bundle install
38
+ bin/rails g tramway:install
31
39
  ```
32
40
 
41
+ The install generator adds the required gems (`haml-rails`, `kaminari`, `view_component`, and `dry-initializer`) to your
42
+ application's Gemfile—if they are not present—and appends the Tailwind safelist configuration Tramway ships with.
43
+
33
44
  ## Getting Started
34
45
 
35
46
  **Step 1**
@@ -68,7 +79,9 @@ end
68
79
 
69
80
  **Step 4**
70
81
 
71
- Copy this [file](https://github.com/Purple-Magic/tramway/blob/main/config/tailwind.config.js) to config/tailwind.config.js
82
+ If you ran `bin/rails g tramway:install`, the Tailwind safelist was already appended to `config/tailwind.config.js`.
83
+ Otherwise, copy this [file](https://github.com/Purple-Magic/tramway/blob/main/config/tailwind.config.js) to
84
+ `config/tailwind.config.js`.
72
85
 
73
86
 
74
87
  **Step 5**
@@ -528,6 +541,38 @@ Tramway provides a responsive, tailwind-styled table with light and dark themes.
528
541
  Another
529
542
  ```
530
543
 
544
+ `Tailwinds::TableComponent` accepts an optional `options` hash that is merged into the outer `.div-table` element. The hash is
545
+ forwarded as HTML attributes, so you can pass things like `id`, `data` attributes, or additional classes. If you do not supply
546
+ your own width utility (e.g. a class that starts with `w-`), the component automatically appends `w-full` to keep the table
547
+ responsive. This allows you to extend the default styling without losing the sensible defaults provided by the component.
548
+
549
+ ```haml
550
+ = component 'tailwinds/table', options: { class: 'max-w-3xl border border-gray-200', data: { controller: 'table' } } do
551
+ = component 'tailwinds/table/header', headers: ['Name', 'Email']
552
+ = component 'tailwinds/table/row' do
553
+ = component 'tailwinds/table/cell' do
554
+ = user.name
555
+ = component 'tailwinds/table/cell' do
556
+ = user.email
557
+ ```
558
+
559
+ When you render a header you can either pass the `headers:` array, as in the examples above, or render custom header content in
560
+ the block. `Tailwinds::Table::HeaderComponent` uses the length of the `headers` array to build the grid if the array is present.
561
+ If you omit the array and provide custom content, pass the `columns:` argument so the component knows how many grid columns to
562
+ generate.
563
+
564
+ ```haml
565
+ = component 'tailwinds/table/header', columns: 4 do
566
+ = component 'tailwinds/table/cell' do
567
+ Custom header cell
568
+ = component 'tailwinds/table/cell' do
569
+ Another header cell
570
+ / ...
571
+ ```
572
+
573
+ With this approach you control the header layout while still benefiting from the default Tailwind grid classes that the header
574
+ component applies.
575
+
531
576
  ### Tailwind-styled forms
532
577
 
533
578
  Tramway uses [Tailwind](https://tailwindcss.com/) by default. All UI helpers are implemented with [ViewComponent](https://github.com/viewcomponent/view_component).
@@ -539,6 +584,7 @@ Tramway provides `tramway_form_for` helper that renders Tailwind-styled forms by
539
584
  ```ruby
540
585
  = tramway_form_for @user do |f|
541
586
  = f.text_field :text
587
+ = f.email_field :email
542
588
  = f.password_field :password
543
589
  = f.select :role, [:admin, :user]
544
590
  = f.multiselect :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']]
@@ -550,12 +596,38 @@ will render [this](https://play.tailwindcss.com/xho3LfjKkK)
550
596
 
551
597
  Available form helpers:
552
598
  * text_field
599
+ * email_field
553
600
  * password_field
554
601
  * file_field
555
602
  * select
556
603
  * multiselect ([Stimulus-based](https://github.com/Purple-Magic/tramway#stimulus-based-inputs))
557
604
  * submit
558
605
 
606
+ **Examples**
607
+
608
+ 1. Sign In Form for `devise` authentication
609
+
610
+ *app/views/devise/sessions/new.html.haml*
611
+ ```haml
612
+ = tramway_form_for(resource, as: resource_name, url: session_path(resource_name), class: 'bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4') do |f|
613
+ = component 'forms/errors', record: resource
614
+
615
+ = f.text_field :email, placeholder: "Your email"
616
+ = f.password_field :password, placeholder: "Your password"
617
+
618
+ = f.submit "Sign In"
619
+ ```
620
+
621
+ 2. Sign In Form for Rails authorization
622
+
623
+ *app/views/sessions/new.html.haml*
624
+ ```haml
625
+ = form_with url: login_path, scope: :session, local: true, builder: Tailwinds::Form::Builder do |form|
626
+ = form.email_field :email
627
+ = form.password_field :password
628
+ = form.submit "Log in"
629
+ ```
630
+
559
631
  #### Stimulus-based inputs
560
632
 
561
633
  `tramway_form_for` provides Tailwind-styled Stimulus-based custom inputs.
@@ -0,0 +1,2 @@
1
+ %a.btn.btn-delete.bg-orange-500.hover:bg-orange-700.text-white.font-bold.py-2.px-4.rounded.ml-2{ onclick: "window.history.back(); return false;", href: '#' }
2
+ = t('actions.back')
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ # Backbutton component
5
+ class BackButtonComponent < BaseComponent
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ # Shared base component for Tailwinds components
5
+ class BaseComponent < Tramway::Component::Base
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ - if text.present?
2
+ - if method == :get
3
+ = link_to text, path, class: classes, **options.except(:class)
4
+ - else
5
+ = button_to text, path, method:, class: classes, **options.except(:class)
6
+ - else
7
+ - if method == :get
8
+ = link_to path, class: classes, **options.except(:class) do
9
+ = content
10
+ - else
11
+ = button_to path, method:, class: classes, **options.except(:class) do
12
+ = content
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ # Default Tramway button
5
+ #
6
+ class ButtonComponent < BaseComponent
7
+ option :text, optional: true, default: -> {}
8
+ option :path
9
+ option :color, default: -> { :blue }
10
+ option :type, default: -> { :default }
11
+ option :size, default: -> { :middle }
12
+ option :method, optional: true, default: -> { :get }
13
+ option :options, optional: true, default: -> { {} }
14
+
15
+ def size_classes
16
+ case size
17
+ when :small
18
+ 'text-sm py-1 px-1'
19
+ when :middle
20
+ 'py-2 px-4'
21
+ end
22
+ end
23
+
24
+ def classes
25
+ (default_classes +
26
+ light_mode_classes +
27
+ dark_mode_classes +
28
+ (method == :get ? %w[px-1 h-fit] : ['cursor-pointer'])).compact.join(' ')
29
+ end
30
+
31
+ def default_classes
32
+ [
33
+ 'btn',
34
+ 'btn-primary',
35
+ 'font-bold',
36
+ 'rounded-sm',
37
+ 'flex',
38
+ 'flex-row',
39
+ size_classes.to_s,
40
+ options[:class].to_s
41
+ ]
42
+ end
43
+
44
+ def light_mode_classes
45
+ [
46
+ "bg-#{color}-500",
47
+ "hover:bg-#{color}-700",
48
+ 'text-white'
49
+ ]
50
+ end
51
+
52
+ def dark_mode_classes
53
+ [
54
+ "dark:bg-#{color}-600",
55
+ "dark:hover:bg-#{color}-800",
56
+ 'dark:text-gray-300'
57
+ ]
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,3 @@
1
+ .container.p-4.flex.align-center.justify-center.w-full.mx-auto.bg-gray-100.dark:bg-gray-900.text-gray-900.dark:text-white{ id: }
2
+ .flex.flex-col.justify-center.w-full.py-8.dark:text-white
3
+ = content
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tailwinds
4
+ module Containers
5
+ # Default page container in Tramway
6
+ class NarrowComponent < Tramway::Component::Base
7
+ option :id, optional: true, default: proc { SecureRandom.uuid }
8
+ end
9
+ end
10
+ end
@@ -11,34 +11,33 @@ module Tailwinds
11
11
  @form_size = options[:size] || options['size'] || :middle
12
12
  end
13
13
 
14
- def text_field(attribute, **options, &)
14
+ def common_field(component_name, input_method, attribute, **options, &)
15
15
  sanitized_options = sanitize_options(options)
16
16
 
17
- render(Tailwinds::Form::TextFieldComponent.new(
18
- input: input(:text_field),
17
+ component_class = "Tailwinds::Form::#{component_name.to_s.camelize}Component".constantize
18
+
19
+ render(component_class.new(
20
+ input: input(input_method),
19
21
  value: get_value(attribute, sanitized_options),
20
22
  **default_options(attribute, sanitized_options)
21
- ), &)
23
+ ),
24
+ &)
22
25
  end
23
26
 
24
- def number_field(attribute, **options, &)
25
- sanitized_options = sanitize_options(options)
27
+ def text_field(attribute, **, &)
28
+ common_field(:text_field, :text_field, attribute, **, &)
29
+ end
26
30
 
27
- render(Tailwinds::Form::NumberFieldComponent.new(
28
- input: input(:number_field),
29
- value: get_value(attribute, sanitized_options),
30
- **default_options(attribute, sanitized_options)
31
- ), &)
31
+ def email_field(attribute, **, &)
32
+ common_field(:text_field, :email_field, attribute, **, &)
32
33
  end
33
34
 
34
- def text_area(attribute, **options, &)
35
- sanitized_options = sanitize_options(options)
35
+ def number_field(attribute, **, &)
36
+ common_field(:number_field, :number_field, attribute, **, &)
37
+ end
36
38
 
37
- render(Tailwinds::Form::TextAreaComponent.new(
38
- input: input(:text_area),
39
- value: get_value(attribute, sanitized_options),
40
- **default_options(attribute, sanitized_options)
41
- ), &)
39
+ def text_area(attribute, **, &)
40
+ common_field(:text_area, :text_area, attribute, **, &)
42
41
  end
43
42
 
44
43
  def password_field(attribute, **options, &)
@@ -95,17 +94,11 @@ module Tailwinds
95
94
  end
96
95
 
97
96
  def get_value(attribute, options)
98
- options[:value] || object.public_send(attribute)
97
+ options[:value] || (object.present? ? object.public_send(attribute) : nil)
99
98
  end
100
99
 
101
100
  def default_options(attribute, options)
102
- {
103
- attribute:,
104
- label: label_build(attribute, options),
105
- for: for_id(attribute),
106
- options:,
107
- size: form_size
108
- }
101
+ { attribute:, label: label_build(attribute, options), for: for_id(attribute), options:, size: form_size }
109
102
  end
110
103
 
111
104
  # :reek:UtilityFunction
@@ -1,8 +1,8 @@
1
1
  .mb-4
2
- - if @label
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 }
7
- = @label
2
+ - if @label
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 }
7
+ = @label
8
8
  = @input
@@ -2,11 +2,4 @@
2
2
  - base_classes = 'bg-transparent appearance-none outline-none h-full w-full text-gray-800 hidden '
3
3
  - base_classes += 'dark:text-white dark:placeholder-white'
4
4
  - classes = "#{@size_class} #{base_classes}"
5
- = @input.call(
6
- @attribute,
7
- @options.merge(
8
- placeholder: "{{placeholder}}",
9
- class: classes,
10
- data: { 'multiselect-target' => 'hiddenInput' }
11
- )
12
- )
5
+ = @input.call(@attribute, @options.merge(placeholder: "{{placeholder}}", class: classes, data: { 'multiselect-target' => 'hiddenInput' }))
@@ -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'
@@ -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?
@@ -1,13 +1,25 @@
1
1
  module.exports = {
2
2
  safelist: [
3
+ // === Navbar ===
4
+ 'ml-4',
5
+
6
+ // === Custom table layout utilities ===
3
7
  'div-table',
4
8
  'div-table-row',
5
9
  'div-table-cell',
10
+ 'sm:text-base',
11
+ 'last:border-b-0',
12
+
13
+ // === Visibility and typography helpers ===
6
14
  'hidden',
15
+ 'xl:hidden',
7
16
  'text-xl',
8
17
  'text-4xl',
9
18
  'font-bold',
10
- 'xl:hidden',
19
+ 'text-right',
20
+
21
+ // === Grid templates used for configurable layouts ===
22
+ 'grid',
11
23
  'grid-cols-1',
12
24
  'grid-cols-2',
13
25
  'grid-cols-3',
@@ -18,44 +30,112 @@ module.exports = {
18
30
  'grid-cols-8',
19
31
  'grid-cols-9',
20
32
  'grid-cols-10',
21
- 'text-right',
22
- 'w-2/3',
33
+
34
+ // === Page container and alignment helpers ===
35
+ 'container',
36
+ 'mx-auto',
37
+ 'align-center',
38
+ 'justify-center',
39
+
40
+ // === Flexbox layout utilities ===
23
41
  'flex',
24
- 'bg-purple-700',
42
+ 'flex-col',
43
+ 'flex-wrap',
44
+ 'flex-row-reverse',
45
+ 'flex-auto',
46
+ 'flex-initial',
47
+ 'justify-between',
48
+ 'justify-end',
49
+ 'space-x-1',
50
+ 'items-center',
51
+
52
+ // === Responsive visibility helpers ===
53
+ 'sm:hidden',
54
+ 'sm:flex',
55
+
56
+ // === Sizing utilities ===
57
+ 'w-2/3',
58
+ 'w-full',
59
+ 'w-fit',
60
+ 'w-8',
61
+ 'min-w-96',
62
+ 'max-w-full',
63
+
64
+ // === Spacing utilities ===
65
+ 'p-4',
25
66
  'px-6',
26
- 'px-3',
27
67
  'px-4',
68
+ 'px-3',
69
+ 'py-8',
28
70
  'py-4',
29
71
  'py-2',
30
72
  'mb-2',
31
- 'dark:placeholder-gray-400',
73
+ 'mt-8',
74
+ 'mt-2',
75
+
76
+ // === Pagination styles ===
77
+ 'bg-white',
78
+ 'rounded-md',
79
+ 'hover:bg-purple-100',
80
+ 'hover:bg-gray-100',
81
+ 'hover:bg-gray-300',
82
+ 'hover:text-gray-800',
83
+
84
+ // === Dark mode pagination styles ===
32
85
  'dark:text-white',
33
86
  'dark:bg-gray-800',
34
- 'dark:bg-gray-900',
35
87
  'dark:hover:bg-gray-700',
88
+ 'dark:bg-gray-900',
36
89
  'dark:bg-gray-700',
37
90
  'dark:text-gray-400',
38
- 'dark:hover:bg-gray-700',
39
91
  'dark:hover:text-gray-400',
40
- 'ml-4',
41
- 'sm:text-base',
42
- 'sm:hidden',
43
- 'sm:flex',
44
- 'last:border-b-0',
45
- 'hover:bg-gray-100',
46
- 'hover:bg-gray-300',
47
- 'hover:text-gray-800',
48
- 'mt-8',
49
- 'justify-between',
50
- 'space-x-1',
51
- 'justify-end',
52
- 'mt-2',
92
+ 'dark:placeholder-gray-400',
93
+
94
+ // === Button and badge helpers ===
95
+ 'bg-purple-700',
96
+ 'rounded',
97
+ 'rounded-full',
98
+ 'rounded-t',
99
+ 'font-medium',
100
+ 'font-normal',
101
+ 'text-xs',
102
+
103
+ // === Form state helpers ===
53
104
  'disabled:bg-gray-100',
54
105
  'disabled:text-gray-400',
55
106
  'disabled:cursor-not-allowed',
56
- // pagination
57
- 'bg-white', 'rounded-md', 'hover:bg-purple-100', 'dark:text-white', 'dark:bg-gray-800', 'dark:hover:bg-gray-700',
58
- // multiselect styles
59
- 'absolute', 'shadow', 'top-11', 'z-40', 'w-full', 'lef-0', 'rounded', 'max-h-select', 'overflow-y-auto', 'cursor-pointer', 'rounded-t', 'border-b', 'hover:bg-teal-100', 'items-center', 'border-transparent', 'border-l-2,', 'relative', 'hover:border-teal-100', 'leading-6', 'bg-transparent', 'appearance-none', 'outline-none', 'h-full' , 'justify-center', 'm-1', 'font-medium', 'rounded-full', 'text-teal-700', 'bg-teal-100', 'border', 'border-teal-300', 'text-xs', 'font-normal', 'leading-none', 'max-w-full', 'flex-initial', 'flex-auto', 'flex-row-reverse',, 'flex-col', 'min-w-96', 'w-fit', 'flex-wrap', 'w-8', 'border-l',
107
+ 'mb-4',
108
+ 'bg-red-500',
109
+ 'hover:bg-red-700',
110
+
111
+ // === Multiselect dropdown positioning ===
112
+ 'absolute',
113
+ 'relative',
114
+ 'shadow',
115
+ 'top-11',
116
+ 'z-40',
117
+ 'left-0',
118
+ 'max-h-select',
119
+ 'overflow-y-auto',
120
+ 'cursor-pointer',
121
+
122
+ // === Multiselect option styling ===
123
+ 'border-b',
124
+ 'border',
125
+ 'border-l',
126
+ 'border-l-2',
127
+ 'border-transparent',
128
+ 'border-teal-300',
129
+ 'hover:border-teal-100',
130
+ 'bg-teal-100',
131
+ 'bg-transparent',
132
+ 'text-teal-700',
133
+ 'leading-6',
134
+ 'leading-none',
135
+ 'appearance-none',
136
+ 'outline-none',
137
+ 'h-full',
138
+ 'm-1',
139
+ 'hover:bg-teal-100',
60
140
  ],
61
141
  }
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'fileutils'
5
+
6
+ module Tramway
7
+ module Generators
8
+ # Running `rails generate tramway:install` will invoke this generator
9
+ #
10
+ class InstallGenerator < Rails::Generators::Base
11
+ desc 'Installs Tramway dependencies and Tailwind safelist configuration.'
12
+
13
+ def ensure_dependencies
14
+ missing_dependencies = gem_dependencies.reject do |dependency|
15
+ gemfile_contains?(dependency[:name])
16
+ end
17
+
18
+ return if missing_dependencies.empty?
19
+
20
+ append_to_file 'Gemfile', <<~GEMS
21
+
22
+ # Tramway dependencies
23
+ #{missing_dependencies.pluck(:declaration).join("\n")}
24
+
25
+ GEMS
26
+ end
27
+
28
+ def ensure_tailwind_safelist
29
+ return create_tailwind_config unless File.exist?(tailwind_config_path)
30
+
31
+ source_entries = extract_safelist_entries(File.read(gem_tailwind_config_path))
32
+ target_content = File.read(tailwind_config_path)
33
+ target_entries = extract_safelist_entries(target_content)
34
+
35
+ missing_entries = source_entries - target_entries
36
+ return if missing_entries.empty?
37
+
38
+ File.write(tailwind_config_path, insert_entries(target_content, missing_entries))
39
+ end
40
+
41
+ def ensure_tailwind_application_stylesheet
42
+ path = tailwind_application_stylesheet_path
43
+ FileUtils.mkdir_p(File.dirname(path))
44
+
45
+ return create_file(path, "#{tailwind_css_import_line}\n") unless File.exist?(path)
46
+
47
+ content = File.read(path)
48
+ return if content.include?(tailwind_css_import_line)
49
+
50
+ File.open(path, 'a') do |file|
51
+ file.write("\n") unless content.empty? || content.end_with?("\n")
52
+ file.write("#{tailwind_css_import_line}\n")
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def gem_dependencies
59
+ @gem_dependencies ||= [
60
+ { name: 'haml-rails', declaration: 'gem "haml-rails"' },
61
+ { name: 'kaminari', declaration: 'gem "kaminari"' },
62
+ { name: 'view_component', declaration: 'gem "view_component"' },
63
+ { name: 'dry-initializer', declaration: "gem 'dry-initializer'" }
64
+ ]
65
+ end
66
+
67
+ def gemfile_path
68
+ @gemfile_path ||= File.join(destination_root, 'Gemfile')
69
+ end
70
+
71
+ def gemfile_contains?(name)
72
+ return false unless File.exist?(gemfile_path)
73
+
74
+ content = File.read(gemfile_path)
75
+ content.match?(/^\s*gem ['"]#{Regexp.escape(name)}['"]/)
76
+ end
77
+
78
+ def tailwind_config_path
79
+ @tailwind_config_path ||= File.join(destination_root, 'config/tailwind.config.js')
80
+ end
81
+
82
+ def gem_tailwind_config_path
83
+ @gem_tailwind_config_path ||= File.expand_path('../../../../config/tailwind.config.js', __dir__)
84
+ end
85
+
86
+ def tailwind_application_stylesheet_path
87
+ @tailwind_application_stylesheet_path ||= File.join(destination_root, 'app/assets/tailwind/application.css')
88
+ end
89
+
90
+ def tailwind_css_import_line
91
+ '@import "tailwindcss";'
92
+ end
93
+
94
+ def create_tailwind_config
95
+ create_file tailwind_config_path, File.read(gem_tailwind_config_path)
96
+ end
97
+
98
+ def extract_safelist_entries(content)
99
+ section = safelist_section(content)
100
+ return [] if section.nil?
101
+
102
+ section.scan(/'([^']+)'/).flatten.map(&:strip).reject(&:empty?).uniq
103
+ end
104
+
105
+ def safelist_section(content)
106
+ match = content.match(/safelist\s*:\s*\[(.*?)\]\s*,?/m)
107
+ match&.[](1)
108
+ end
109
+
110
+ def insert_entries(content, entries)
111
+ match = content.match(/safelist\s*:\s*\[(.*?)\](\s*,?)/m)
112
+ return content unless match
113
+
114
+ closing_index = match.begin(0) + match[0].rindex(']')
115
+ indentation = closing_indentation(content, closing_index)
116
+ formatted_entries = entries.map { |entry| "#{indentation}'#{entry}',\n" }.join
117
+
118
+ needs_leading_newline = content[closing_index - 1] != "\n"
119
+ insertion = String.new
120
+ insertion << "\n" if needs_leading_newline
121
+ insertion << formatted_entries
122
+
123
+ content.dup.insert(closing_index, insertion)
124
+ end
125
+
126
+ def closing_indentation(content, index)
127
+ line_start = content.rindex("\n", index - 1) || 0
128
+ line = content[line_start..index]
129
+ line[/^\s*/] || ' '
130
+ end
131
+ end
132
+ end
133
+ end
@@ -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
 
@@ -12,6 +12,45 @@ module Tramway
12
12
  &
13
13
  )
14
14
  end
15
+
16
+ def tramway_table(**options, &)
17
+ component 'tailwinds/table', options:, &
18
+ end
19
+
20
+ def tramway_row(**options, &)
21
+ component 'tailwinds/table/row',
22
+ cells: options.delete(:cells),
23
+ href: options.delete(:href),
24
+ options:,
25
+ &
26
+ end
27
+
28
+ def tramway_cell(&)
29
+ component 'tailwinds/table/cell', &
30
+ end
31
+
32
+ def tramway_button(path:, text: nil, method: :get, **options)
33
+ component 'tailwinds/button',
34
+ text:,
35
+ path:,
36
+ method:,
37
+ color: options.delete(:color),
38
+ type: options.delete(:type),
39
+ size: options.delete(:size),
40
+ options:
41
+ end
42
+
43
+ def tramway_back_button
44
+ component 'tailwinds/back_button'
45
+ end
46
+
47
+ def tramway_container(id: nil, &)
48
+ if id.present?
49
+ component 'tailwinds/containers/narrow', id: id, &
50
+ else
51
+ component 'tailwinds/containers/narrow', &
52
+ end
53
+ end
15
54
  end
16
55
  end
17
56
  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.5'
4
+ VERSION = '0.6'
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.5
4
+ version: '0.6'
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-10-14 00:00:00.000000000 Z
12
+ date: 2025-10-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: anyway_config
@@ -143,6 +143,13 @@ files:
143
143
  - app/assets/javascripts/tramway/table_row_preview_controller.js
144
144
  - app/components/tailwind_component.html.haml
145
145
  - app/components/tailwind_component.rb
146
+ - app/components/tailwinds/back_button_component.html.haml
147
+ - app/components/tailwinds/back_button_component.rb
148
+ - app/components/tailwinds/base_component.rb
149
+ - app/components/tailwinds/button_component.html.haml
150
+ - app/components/tailwinds/button_component.rb
151
+ - app/components/tailwinds/containers/narrow_component.html.haml
152
+ - app/components/tailwinds/containers/narrow_component.rb
146
153
  - app/components/tailwinds/form/builder.rb
147
154
  - app/components/tailwinds/form/file_field_component.html.haml
148
155
  - app/components/tailwinds/form/file_field_component.rb
@@ -214,6 +221,7 @@ files:
214
221
  - app/views/tramway/layouts/application.html.haml
215
222
  - config/routes.rb
216
223
  - config/tailwind.config.js
224
+ - lib/generators/tramway/install/install_generator.rb
217
225
  - lib/kaminari/helpers/tag.rb
218
226
  - lib/rules/turbo_html_attributes_rules.rb
219
227
  - lib/tasks/tramway_tasks.rake