tramway 0.5.5.1 → 0.6.0.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: 7e2e7bd75d61196a4ba4302fe3c05d5dfce6928a35ea0d97022b9170ce911ca8
4
- data.tar.gz: 1b89b1fb5a18ae6d28c8afb1b8a1d94da832f0855c1acf1f5ab4f9fb0f1d4bd7
3
+ metadata.gz: a7d9322ff7e374d37c137d366bec17c0728b350eccf87b146655d7704f1e560d
4
+ data.tar.gz: 045e0e118d1269095a241ce783ffb08bc282b8e885570341ad57878e4e0e1ac4
5
5
  SHA512:
6
- metadata.gz: 4565144363a3419833bb267231fc403ff49c1da50caa3ee4ec936d6a2245fc2ddf89ab111af33cc7a5759cd9bc51d90cc456e5108acc78b73e99ca7ea5107158
7
- data.tar.gz: a83591d638a439dcd659474cfd50b73f9f292bff11fd2e18987c821df25888a9de4ae68d16fb2d7048eb36949869350339b8c3104ffaa71f92f97042d2a84ba9
6
+ metadata.gz: 4d4594294bf08a6822a7b4a4d0bb8d777fdaed41fe0024d3fa29a5741c4c46c63ea921ea7c1742888502c42b5195e7198a23e4a80e8dc4216557e40da1e20953
7
+ data.tar.gz: e2e6f019d9e2b19e8fa32f6d3740c2a32473b9e070845b04756e20e53eb2abb9f51b37dd2aa721bbad98a557e90f55eb32054ed36f1a014056be4dfa8d3018a9
data/README.md CHANGED
@@ -29,17 +29,18 @@ Add this line to your application's Gemfile:
29
29
 
30
30
  ```ruby
31
31
  gem "tramway"
32
- gem "haml-rails"
33
- gem "kaminari"
34
- gem "view_component"
35
32
  ```
36
33
 
37
- OR
34
+ Then install Tramway and its dependencies:
38
35
 
39
36
  ```shell
40
- bundle add tramway view_component kaminari view_component
37
+ bundle install
38
+ bin/rails g tramway:install
41
39
  ```
42
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
+
43
44
  ## Getting Started
44
45
 
45
46
  **Step 1**
@@ -78,7 +79,9 @@ end
78
79
 
79
80
  **Step 4**
80
81
 
81
- 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`.
82
85
 
83
86
 
84
87
  **Step 5**
@@ -130,13 +133,30 @@ Tramway Entity supports several options that are used in different features.
130
133
  ```ruby
131
134
  Tramway.configure do |config|
132
135
  config.entities = [
133
- { name: :user, route: { namespace: :admin } }, # `admin_users_path` link in the Tramway Navbar
134
- { name: :podcast, route: { route_method: :shows } }, # `shows_path` link in the Tramway Navbar
135
- { name: :episodes, route: { namespace: :podcasts, route_method: :episodes } }, # `podcasts_episodes_path` link in the Tramway Navbar
136
+ {
137
+ name: :user,
138
+ route: { namespace: :admin }
139
+ }, # `/admin/users` link in the Tramway Navbar
140
+ {
141
+ name: :episodes,
142
+ route: {
143
+ namespace: :podcasts,
144
+ route_method: :episodes
145
+ }
146
+ }, # `/podcasts/episodes` link in the Tramway Navbar
136
147
  ]
137
148
  end
138
149
  ```
139
150
 
151
+ **route_helper**
152
+
153
+ To get routes Tramway generated just Tramway::Engine.
154
+
155
+ ```ruby
156
+ Tramway::Engine.routes.url_helpers.users_path => '/admin/users'
157
+ Tramway::Engine.routes.url_helpers.podcasts_episodes_path => '/podcasts/episodes'
158
+ ```
159
+
140
160
  ### Tramway Decorators
141
161
 
142
162
  Tramway provides convenient decorators for your objects. **NOTE:** This is not the decorator pattern in its usual representation.
@@ -452,15 +472,19 @@ tramway_navbar title: 'Purple Magic', background: { color: :red, intensity: 500
452
472
  end
453
473
  ```
454
474
 
455
- # Haml example
475
+ # ERB example
456
476
 
457
- ```haml
458
- = tramway_navbar title: 'Purple Magic', background: { color: :red, intensity: 500 } do |nav|
459
- - nav.left do
460
- - nav.item 'Users', '/users'
461
- - nav.item 'Podcasts', '/podcasts'
462
- - nav.right do
463
- - nav.item 'Sign out', '/users/sessions', method: :delete, confirm: 'Wanna quit?'
477
+ ```erb
478
+ <%= tramway_navbar title: 'Purple Magic', background: { color: :red, intensity: 500 } do |nav| %>
479
+ <% nav.left do %>
480
+ <%= nav.item 'Users', '/users' %>
481
+ <%= nav.item 'Podcasts', '/podcasts' %>
482
+ <% end %>
483
+
484
+ <% nav.right do %>
485
+ <%= nav.item 'Sign out', '/users/sessions', method: :delete, confirm: 'Wanna quit?' %>
486
+ <% end %>
487
+ <% end %>
464
488
  ```
465
489
 
466
490
  will render [this](https://play.tailwindcss.com/UZPTCudFw5)
@@ -482,13 +506,16 @@ with_entities: Show Tramway Entities index page links to navbar. Default: true
482
506
 
483
507
  In case you want to hide entity links you can pass `with_entities: false`.
484
508
 
485
- ```haml
486
- - if current_user.present?
487
- = tramway_navbar title: 'WaiWai' do |nav|
488
- - nav.left do
489
- - nav.item 'Board', admin_board_path
490
- - else
491
- = tramway_navbar title: 'WaiWai', with_entities: false
509
+ ```erb
510
+ <% if current_user.present? %>
511
+ <%= tramway_navbar title: 'WaiWai' do |nav| %>
512
+ <% nav.left do %>
513
+ <%= nav.item 'Board', admin_board_path %>
514
+ <% end %>
515
+ <% end %>
516
+ <% else %>
517
+ <%= tramway_navbar title: 'WaiWai', with_entities: false %>
518
+ <% end %>
492
519
  ```
493
520
 
494
521
  #### nav.left and nav.right
@@ -526,31 +553,43 @@ end
526
553
 
527
554
  ### Tramway Table Component
528
555
 
529
- Tramway provides a responsive, tailwind-styled table with light and dark themes.
556
+ Tramway provides a responsive, tailwind-styled table with light and dark themes. Use the `tramway_table`, `tramway_row`, and
557
+ `tramway_cell` helpers to build tables with readable ERB templates while still leveraging the underlying ViewComponent
558
+ implementations.
559
+
560
+ ```erb
561
+ <%= tramway_table do %>
562
+ <%= component 'tailwinds/table/header', headers: ['Column 1', 'Column 2'] %>
530
563
 
531
- ```haml
532
- = component 'tailwinds/table' do
533
- = component 'tailwinds/table/header', headers: ['Column 1', 'Column 2']
534
- = component 'tailwinds/table/row' do
535
- = component 'tailwinds/table/cell' do
564
+ <%= tramway_row do %>
565
+ <%= tramway_cell do %>
536
566
  Something
537
- = component 'tailwinds/table/cell' do
567
+ <% end %>
568
+ <%= tramway_cell do %>
538
569
  Another
570
+ <% end %>
571
+ <% end %>
572
+ <% end %>
539
573
  ```
540
574
 
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.
575
+ `tramway_table` accepts the same optional `options` hash as `Tailwinds::TableComponent`. The hash is forwarded as HTML
576
+ attributes, so you can pass things like `id`, `data` attributes, or additional classes. If you do not supply your own width
577
+ utility (e.g. a class that starts with `w-`), the component automatically appends `w-full` to keep the table responsive. This
578
+ allows you to extend the default styling without losing the sensible defaults provided by the component.
579
+
580
+ ```erb
581
+ <%= tramway_table class: 'max-w-3xl border border-gray-200', data: { controller: 'table' } do %>
582
+ <%= component 'tailwinds/table/header', headers: ['Name', 'Email'] %>
545
583
 
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
584
+ <%= tramway_row do %>
585
+ <%= tramway_cell do %>
586
+ <%= user.name %>
587
+ <% end %>
588
+ <%= tramway_cell do %>
589
+ <%= user.email %>
590
+ <% end %>
591
+ <% end %>
592
+ <% end %>
554
593
  ```
555
594
 
556
595
  When you render a header you can either pass the `headers:` array, as in the examples above, or render custom header content in
@@ -558,18 +597,48 @@ the block. `Tailwinds::Table::HeaderComponent` uses the length of the `headers`
558
597
  If you omit the array and provide custom content, pass the `columns:` argument so the component knows how many grid columns to
559
598
  generate.
560
599
 
561
- ```haml
562
- = component 'tailwinds/table/header', columns: 4 do
563
- = component 'tailwinds/table/cell' do
600
+ ```erb
601
+ <%= component 'tailwinds/table/header', columns: 4 do %>
602
+ <%= tramway_cell do %>
564
603
  Custom header cell
565
- = component 'tailwinds/table/cell' do
604
+ <% end %>
605
+ <%= tramway_cell do %>
566
606
  Another header cell
567
- / ...
607
+ <% end %>
608
+ <!-- ... -->
609
+ <% end %>
568
610
  ```
569
611
 
570
612
  With this approach you control the header layout while still benefiting from the default Tailwind grid classes that the header
571
613
  component applies.
572
614
 
615
+ ### Tramway Buttons and Containers
616
+
617
+ Tramway ships with helpers for common UI patterns built on top of Tailwind components.
618
+
619
+ * `tramway_button` renders a button-styled link and accepts `path`, optional `text`, HTTP `method`, and styling options such as
620
+ `color`, `type`, and `size`. All additional keyword arguments are forwarded to the underlying component as HTML attributes.
621
+
622
+ ```erb
623
+ <%= tramway_button path: user_path(user), text: 'View profile', color: :emerald, data: { turbo: false } %>
624
+ ```
625
+
626
+ * `tramway_back_button` renders a standardized "Back" link.
627
+
628
+ ```erb
629
+ <%= tramway_back_button %>
630
+ ```
631
+
632
+ * `tramway_container` wraps content in a responsive, narrow layout container. Pass an `id` if you need to target the container
633
+ with JavaScript or CSS.
634
+
635
+ ```erb
636
+ <%= tramway_container id: 'user-settings' do %>
637
+ <h2 class="text-xl font-semibold">Settings</h2>
638
+ <p class="mt-2 text-gray-600">Update your preferences below.</p>
639
+ <% end %>
640
+ ```
641
+
573
642
  ### Tailwind-styled forms
574
643
 
575
644
  Tramway uses [Tailwind](https://tailwindcss.com/) by default. All UI helpers are implemented with [ViewComponent](https://github.com/viewcomponent/view_component).
@@ -578,26 +647,56 @@ Tramway uses [Tailwind](https://tailwindcss.com/) by default. All UI helpers are
578
647
 
579
648
  Tramway provides `tramway_form_for` helper that renders Tailwind-styled forms by default.
580
649
 
581
- ```ruby
582
- = tramway_form_for @user do |f|
583
- = f.text_field :text
584
- = f.password_field :password
585
- = f.select :role, [:admin, :user]
586
- = f.multiselect :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']]
587
- = f.file_field :file
588
- = f.submit "Create User"
650
+ ```erb
651
+ <%= tramway_form_for @user do |f| %>
652
+ <%= f.text_field :text %>
653
+ <%= f.email_field :email %>
654
+ <%= f.password_field :password %>
655
+ <%= f.select :role, [:admin, :user] %>
656
+ <%= f.multiselect :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']] %>
657
+ <%= f.file_field :file %>
658
+ <%= f.submit 'Create User' %>
659
+ <% end %>
589
660
  ```
590
661
 
591
662
  will render [this](https://play.tailwindcss.com/xho3LfjKkK)
592
663
 
593
664
  Available form helpers:
594
665
  * text_field
666
+ * email_field
595
667
  * password_field
596
668
  * file_field
597
669
  * select
598
670
  * multiselect ([Stimulus-based](https://github.com/Purple-Magic/tramway#stimulus-based-inputs))
599
671
  * submit
600
672
 
673
+ **Examples**
674
+
675
+ 1. Sign In Form for `devise` authentication
676
+
677
+ *app/views/devise/sessions/new.html.erb*
678
+ ```erb
679
+ <%= 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| %>
680
+ <%= component 'forms/errors', record: resource %>
681
+
682
+ <%= f.text_field :email, placeholder: 'Your email' %>
683
+ <%= f.password_field :password, placeholder: 'Your password' %>
684
+
685
+ <%= f.submit 'Sign In' %>
686
+ <% end %>
687
+ ```
688
+
689
+ 2. Sign In Form for Rails authorization
690
+
691
+ *app/views/sessions/new.html.erb*
692
+ ```erb
693
+ <%= form_with url: login_path, scope: :session, local: true, builder: Tailwinds::Form::Builder do |form| %>
694
+ <%= form.email_field :email %>
695
+ <%= form.password_field :password %>
696
+ <%= form.submit 'Log in' %>
697
+ <% end %>
698
+ ```
699
+
601
700
  #### Stimulus-based inputs
602
701
 
603
702
  `tramway_form_for` provides Tailwind-styled Stimulus-based custom inputs.
@@ -606,10 +705,11 @@ Available form helpers:
606
705
 
607
706
  In case you want to use tailwind-styled multiselect this way
608
707
 
609
- ```haml
610
- = tramway_form_for @user do |f|
611
- = f.multiselect :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']]
612
- #- ...
708
+ ```erb
709
+ <%= tramway_form_for @user do |f| %>
710
+ <%= f.multiselect :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']] %>
711
+ <%# ... %>
712
+ <% end %>
613
713
  ```
614
714
 
615
715
  you should add Tramway Multiselect Stimulus controller to your application.
@@ -633,9 +733,10 @@ application.register('multiselect', Multiselect) // register Multiselect control
633
733
 
634
734
  Use Stimulus `change` action with Tramway Multiselect
635
735
 
636
- ```ruby
637
- = tramway_form_for @user do |f|
638
- = f.multiselect :role, data: { action: 'change->user-form#updateForm' } # user-form is your Stimulus controller, updateForm is a method inside user-form Stimulus controller
736
+ ```erb
737
+ <%= tramway_form_for @user do |f| %>
738
+ <%= f.multiselect :role, data: { action: 'change->user-form#updateForm' } %>
739
+ <% end %>
639
740
  ```
640
741
 
641
742
  ### Tailwind-styled pagination for Kaminari
@@ -657,9 +758,9 @@ Tramway.configure do |config|
657
758
  end
658
759
  ```
659
760
 
660
- *app/views/users/index.html.haml*
661
- ```haml
662
- = paginate @users # it will render tailwind-styled pagination buttons by default
761
+ *app/views/users/index.html.erb*
762
+ ```erb
763
+ <%= paginate @users %> <%# it will render tailwind-styled pagination buttons by default %>
663
764
  ```
664
765
 
665
766
  Pagination buttons looks like [this](https://play.tailwindcss.com/mqgDS5l9oY)
@@ -680,11 +781,41 @@ user_2 = tramway_form User.first
680
781
  user_2.object #=> returns pure user object
681
782
  ```
682
783
 
784
+ ## Configuration
785
+
786
+ ### Custom layout
787
+
788
+ In case you wanna use a custom layout:
789
+
790
+ 1. Create a controller
791
+ 2. Set the layout there
792
+ 3. Set this controller as `application_controller` in Tramway initializer
793
+ 4. Reload your server
794
+
795
+ **Example**
796
+
797
+ *app/controllers/admin/application_controller.rb*
798
+ ```ruby
799
+ class Admin::ApplicationController < ApplicationController
800
+ layout 'admin/application'
801
+ end
802
+ ```
803
+
804
+ *config/initializers/tramway.rb*
805
+ ```ruby
806
+ Tramway.configure do |config|
807
+ config.application_controller = 'Admin::ApplicationController'
808
+ end
809
+ ```
810
+
683
811
  ## Articles
684
812
  * [Tramway on Rails](https://kalashnikovisme.medium.com/tramway-on-rails-32158c35ed68)
813
+ * [Tramway is the way to deal with little things for Rails developers](https://medium.com/@kalashnikovisme/tramway-is-the-way-to-deal-with-little-things-for-rails-developers-4f502172a18c)
685
814
  * [Delegating ActiveRecord methods to decorators in Rails](https://kalashnikovisme.medium.com/delegating-activerecord-methods-to-decorators-in-rails-4e4ec1c6b3a6)
686
815
  * [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)
687
816
  * [Decorating associations in Rails with Tramway](https://kalashnikovisme.medium.com/decorating-associations-in-rails-with-tramway-b46a28392f9e)
817
+ * [Easy-to-use Tailwind-styled multi-select built with Stimulus](https://medium.com/@kalashnikovisme/easy-to-use-tailwind-styled-multi-select-built-with-stimulus-b3daa9e307aa)
818
+ *
688
819
 
689
820
  ## Contributing
690
821
 
@@ -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,7 +94,7 @@ 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)
@@ -17,12 +17,12 @@
17
17
  %p.text-center.mt-10
18
18
  You should fill class-level method `self.list_attributes` inside your
19
19
  = decorator
20
+ - else
21
+ = component 'tailwinds/table' do
22
+ = component 'tailwinds/table/header', headers: list_attributes.map { |attribute| @model_class.human_attribute_name(attribute) }
23
+ - @entities.each do |item|
24
+ = render 'tramway/entities/entity', entity: item
20
25
 
21
- = component 'tailwinds/table' do
22
- = component 'tailwinds/table/header', headers: list_attributes.map { |attribute| @model_class.human_attribute_name(attribute) }
23
- - @entities.each do |item|
24
- = render 'tramway/entities/entity', entity: item
25
-
26
- - if Tramway.config.pagination[:enabled]
27
- .flex.mt-4
28
- = paginate @entities, custom_path_method: "#{@model_class.model_name.plural}_path"
26
+ - if Tramway.config.pagination[:enabled]
27
+ .flex.mt-4
28
+ = paginate @entities, custom_path_method: "#{@model_class.model_name.plural}_path"
@@ -1 +1,2 @@
1
- = render 'tramway/entities/list'
1
+ = tramway_container do
2
+ = render 'tramway/entities/list'
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tramway
4
- VERSION = '0.5.5.1'
4
+ VERSION = '0.6.0.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.5.1
4
+ version: 0.6.0.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-10-23 00:00:00.000000000 Z
12
+ date: 2025-10-26 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