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 +4 -4
- data/README.md +78 -6
- 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 +19 -26
- data/app/components/tailwinds/form/file_field_component.html.haml +6 -6
- data/app/components/tailwinds/form/multiselect/select_as_input.html.haml +1 -8
- data/app/components/tailwinds/table/row_component.rb +2 -6
- data/app/controllers/tramway/entities_controller.rb +11 -5
- data/config/routes.rb +4 -4
- data/config/tailwind.config.js +105 -25
- data/lib/generators/tramway/install/install_generator.rb +133 -0
- data/lib/tramway/base_form.rb +1 -1
- data/lib/tramway/decorators/association.rb +1 -1
- data/lib/tramway/decorators/class_helper.rb +18 -16
- data/lib/tramway/forms/properties.rb +6 -10
- data/lib/tramway/helpers/views_helper.rb +39 -0
- data/lib/tramway/navbar.rb +5 -5
- 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: ff761d41cd4748bf3d7e1451825da24764f69077b842e92f7be6baf0f14dff55
|
|
4
|
+
data.tar.gz: d9c3b25519ab3eb4dfe9c7d2e26d8dffb339a2743b6c5c494495bd4c5b71fc6c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
34
|
+
Then install Tramway and its dependencies:
|
|
28
35
|
|
|
29
36
|
```shell
|
|
30
|
-
bundle
|
|
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
|
-
|
|
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,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,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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
11
|
-
controller:'/tramway/entities',
|
|
12
|
-
defaults:
|
|
10
|
+
only: [:index],
|
|
11
|
+
controller: '/tramway/entities',
|
|
12
|
+
defaults: { entity: entity }
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
if segments.empty?
|
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
|
data/lib/tramway/base_form.rb
CHANGED
|
@@ -20,26 +20,19 @@ module Tramway
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def decorator_class_name(object_or_array_or_class, namespace)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
data/lib/tramway/navbar.rb
CHANGED
|
@@ -10,13 +10,13 @@ module Tramway
|
|
|
10
10
|
@items = { left: [], right: [] }
|
|
11
11
|
@filling = nil
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
entities = Tramway.config.entities
|
|
13
|
+
return unless with_entities
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
entities = Tramway.config.entities
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
return unless entities.any?
|
|
18
|
+
|
|
19
|
+
preset_left entities
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def left
|
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'
|
|
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-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
|