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 +4 -4
- data/README.md +197 -66
- data/app/components/tailwinds/back_button_component.html.haml +2 -0
- data/app/components/tailwinds/back_button_component.rb +7 -0
- data/app/components/tailwinds/base_component.rb +7 -0
- data/app/components/tailwinds/button_component.html.haml +12 -0
- data/app/components/tailwinds/button_component.rb +60 -0
- data/app/components/tailwinds/containers/narrow_component.html.haml +3 -0
- data/app/components/tailwinds/containers/narrow_component.rb +10 -0
- data/app/components/tailwinds/form/builder.rb +18 -19
- data/app/views/tramway/entities/_list.html.haml +8 -8
- data/app/views/tramway/entities/index.html.haml +2 -1
- data/config/tailwind.config.js +105 -25
- data/lib/generators/tramway/install/install_generator.rb +133 -0
- data/lib/tramway/helpers/views_helper.rb +39 -0
- data/lib/tramway/version.rb +1 -1
- metadata +10 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7d9322ff7e374d37c137d366bec17c0728b350eccf87b146655d7704f1e560d
|
|
4
|
+
data.tar.gz: 045e0e118d1269095a241ce783ffb08bc282b8e885570341ad57878e4e0e1ac4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
34
|
+
Then install Tramway and its dependencies:
|
|
38
35
|
|
|
39
36
|
```shell
|
|
40
|
-
bundle
|
|
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
|
-
|
|
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
|
-
{
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
#
|
|
475
|
+
# ERB example
|
|
456
476
|
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
```
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
532
|
-
|
|
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
|
-
|
|
567
|
+
<% end %>
|
|
568
|
+
<%= tramway_cell do %>
|
|
538
569
|
Another
|
|
570
|
+
<% end %>
|
|
571
|
+
<% end %>
|
|
572
|
+
<% end %>
|
|
539
573
|
```
|
|
540
574
|
|
|
541
|
-
`
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
```
|
|
562
|
-
|
|
563
|
-
|
|
600
|
+
```erb
|
|
601
|
+
<%= component 'tailwinds/table/header', columns: 4 do %>
|
|
602
|
+
<%= tramway_cell do %>
|
|
564
603
|
Custom header cell
|
|
565
|
-
|
|
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
|
-
```
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
```
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
```
|
|
637
|
-
|
|
638
|
-
|
|
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.
|
|
661
|
-
```
|
|
662
|
-
|
|
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,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
|
|
@@ -11,34 +11,33 @@ module Tailwinds
|
|
|
11
11
|
@form_size = options[:size] || options['size'] || :middle
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
14
|
+
def common_field(component_name, input_method, attribute, **options, &)
|
|
15
15
|
sanitized_options = sanitize_options(options)
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
25
|
-
|
|
27
|
+
def text_field(attribute, **, &)
|
|
28
|
+
common_field(:text_field, :text_field, attribute, **, &)
|
|
29
|
+
end
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
35
|
-
|
|
35
|
+
def number_field(attribute, **, &)
|
|
36
|
+
common_field(:number_field, :number_field, attribute, **, &)
|
|
37
|
+
end
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
=
|
|
1
|
+
= tramway_container do
|
|
2
|
+
= render 'tramway/entities/list'
|
data/config/tailwind.config.js
CHANGED
|
@@ -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
|
-
'
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
57
|
-
'bg-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
data/lib/tramway/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tramway
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
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-
|
|
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
|