tramway 3.0.1 → 3.0.3
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 +25 -0
- data/app/assets/javascripts/tramway/table_row_preview_controller.js +100 -6
- data/app/components/tailwind_component.rb +3 -3
- data/app/components/tramway/form/builder.rb +48 -7
- data/app/components/tramway/form/checkbox_component.html.haml +2 -2
- data/app/components/tramway/form/checkbox_component.rb +14 -0
- data/app/components/tramway/form/date_field_component.html.haml +1 -1
- data/app/components/tramway/form/datetime_field_component.html.haml +1 -1
- data/app/components/tramway/form/label_component.html.haml +1 -1
- data/app/components/tramway/form/label_component.rb +2 -1
- data/app/components/tramway/form/number_field_component.html.haml +1 -1
- data/app/components/tramway/form/select_component.html.haml +1 -1
- data/app/components/tramway/form/text_area_component.html.haml +1 -1
- data/app/components/tramway/form/text_field_component.html.haml +1 -1
- data/app/components/tramway/form/time_field_component.html.haml +1 -1
- data/app/components/tramway/form/tramway_select_component.html.haml +1 -1
- data/app/components/tramway/navbar_component.html.haml +80 -31
- data/app/components/tramway/table/row/preview_component.html.haml +18 -2
- data/app/components/tramway/table/row/preview_component.rb +2 -1
- data/app/components/tramway/table/row_component.html.haml +9 -2
- data/app/components/tramway/table/row_component.rb +17 -0
- data/app/components/tramway/table_component.html.haml +0 -2
- data/config/tailwind.config.js +21 -6
- data/docs/AGENTS.md +109 -1
- data/lib/generators/tramway/install/install_generator.rb +77 -2
- data/lib/tramway/helpers/views_helper.rb +7 -6
- data/lib/tramway/utils/field.rb +6 -2
- data/lib/tramway/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1da050191fdef1006bf62ea51fe4d11465eb809c273dc681f55f8937243e53fa
|
|
4
|
+
data.tar.gz: bce78a50e8ce1fdc3b8a126f3797841c5883c3b1810c370abfb00ba9609cc1e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d7c33ecf3b2544fab033a332e303dfd1cb23fa40dfb4adf8201a702be929ad702c96e5246055d59c88fe4c67e74d857590989ad581b61f398be05fd1ebdaac3
|
|
7
|
+
data.tar.gz: 88a49d7eedda98eb23455e4042b21ce67fc8972a0509e780e8742937b73705ae9cfa6d14fb236b3a1da1ef478e384cba009f776c1711bccd5c98d104ae20d32e
|
data/README.md
CHANGED
|
@@ -987,6 +987,20 @@ Use the optional `href:` argument on `tramway_row` to turn an entire row into a
|
|
|
987
987
|
<% end %>
|
|
988
988
|
```
|
|
989
989
|
|
|
990
|
+
`tramway_row` also accepts `preview:`. By default preview is enabled (`true`) for non-linked rows and renders the mobile slide-up
|
|
991
|
+
details panel. Pass `preview: false` when you want a row without the preview panel.
|
|
992
|
+
|
|
993
|
+
```erb
|
|
994
|
+
<%= tramway_row preview: false do %>
|
|
995
|
+
<%= tramway_cell do %>
|
|
996
|
+
<%= user.name %>
|
|
997
|
+
<% end %>
|
|
998
|
+
<%= tramway_cell do %>
|
|
999
|
+
<%= user.email %>
|
|
1000
|
+
<% end %>
|
|
1001
|
+
<% end %>
|
|
1002
|
+
```
|
|
1003
|
+
|
|
990
1004
|
When you render a header you can either pass the `headers:` array, as in the examples above, or render custom header content in
|
|
991
1005
|
the block. `tramway_header` uses the length of the `headers` array to build the grid if the array is present.
|
|
992
1006
|
If you omit the array and provide custom content, pass the `columns:` argument so the component knows how many grid columns to
|
|
@@ -1227,6 +1241,17 @@ In case you need to use Stimulus `change` action with Tramway Select
|
|
|
1227
1241
|
<% end %>
|
|
1228
1242
|
```
|
|
1229
1243
|
|
|
1244
|
+
Remote form example:
|
|
1245
|
+
|
|
1246
|
+
```erb
|
|
1247
|
+
<%= tramway_form_for @user, remote: true do |f| %>
|
|
1248
|
+
<%= f.text_field :name %>
|
|
1249
|
+
<%= f.email_field :email %>
|
|
1250
|
+
<% end %>
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
With `remote: true`, Tramway submits the form on each input `change` via inline JavaScript; no additional controller setup is required.
|
|
1254
|
+
|
|
1230
1255
|
### Tailwind-styled pagination for Kaminari
|
|
1231
1256
|
|
|
1232
1257
|
Tramway uses [Tailwind](https://tailwindcss.com/) by default. It has tailwind-styled pagination for [kaminari](https://github.com/kaminari/kaminari).
|
|
@@ -3,10 +3,25 @@ import { Controller } from "@hotwired/stimulus";
|
|
|
3
3
|
export default class TableRowPreview extends Controller {
|
|
4
4
|
connect() {
|
|
5
5
|
this.items = JSON.parse(this.element.dataset.items || '{}');
|
|
6
|
+
this.attachSwipeGesture();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
disconnect() {
|
|
10
|
+
this.detachSwipeGesture();
|
|
6
11
|
}
|
|
7
12
|
|
|
8
13
|
toggle() {
|
|
9
|
-
const rollUp =
|
|
14
|
+
const rollUp = this.rollUpElement();
|
|
15
|
+
if (!rollUp) return;
|
|
16
|
+
|
|
17
|
+
rollUp.classList.remove("animate-roll-down");
|
|
18
|
+
rollUp.classList.add("animate-roll-up");
|
|
19
|
+
|
|
20
|
+
// Show pre-rendered preview content that is hidden by default.
|
|
21
|
+
if (Object.keys(this.items).length === 0) {
|
|
22
|
+
rollUp.classList.remove("hidden");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
10
25
|
|
|
11
26
|
const existingTable = rollUp.querySelector(".div-table");
|
|
12
27
|
if (existingTable) {
|
|
@@ -18,8 +33,6 @@ export default class TableRowPreview extends Controller {
|
|
|
18
33
|
existingTitle.remove();
|
|
19
34
|
}
|
|
20
35
|
|
|
21
|
-
if (Object.keys(this.items).length === 0) return;
|
|
22
|
-
|
|
23
36
|
const titleText = document.createElement("h3");
|
|
24
37
|
|
|
25
38
|
titleText.classList.add("text-xl");
|
|
@@ -33,12 +46,93 @@ export default class TableRowPreview extends Controller {
|
|
|
33
46
|
rollUp.insertAdjacentElement('afterbegin', table);
|
|
34
47
|
rollUp.insertAdjacentElement('afterbegin', titleText);
|
|
35
48
|
|
|
36
|
-
rollUp.classList.
|
|
49
|
+
rollUp.classList.remove("hidden");
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
close() {
|
|
40
|
-
const rollUp =
|
|
41
|
-
rollUp
|
|
53
|
+
const rollUp = this.rollUpElement();
|
|
54
|
+
if (!rollUp) return;
|
|
55
|
+
|
|
56
|
+
this.resetDragStyles(rollUp);
|
|
57
|
+
rollUp.classList.remove("animate-roll-up");
|
|
58
|
+
rollUp.classList.add("animate-roll-down");
|
|
59
|
+
|
|
60
|
+
rollUp.addEventListener("animationend", () => {
|
|
61
|
+
rollUp.classList.add("hidden");
|
|
62
|
+
rollUp.classList.remove("animate-roll-down");
|
|
63
|
+
rollUp.classList.add("animate-roll-up");
|
|
64
|
+
}, { once: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
rollUpElement() {
|
|
68
|
+
if (this.element.id === "roll-up") return this.element;
|
|
69
|
+
|
|
70
|
+
return this.element.previousElementSibling || document.getElementById("roll-up");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
attachSwipeGesture() {
|
|
74
|
+
if (this.element.id !== "roll-up") return;
|
|
75
|
+
|
|
76
|
+
this.startY = null;
|
|
77
|
+
this.startX = null;
|
|
78
|
+
this.currentDeltaY = 0;
|
|
79
|
+
|
|
80
|
+
this.onTouchStart = (event) => {
|
|
81
|
+
if (this.element.classList.contains("hidden")) return;
|
|
82
|
+
if (event.touches.length !== 1) return;
|
|
83
|
+
|
|
84
|
+
this.startY = event.touches[0].clientY;
|
|
85
|
+
this.startX = event.touches[0].clientX;
|
|
86
|
+
this.currentDeltaY = 0;
|
|
87
|
+
this.element.style.transition = "none";
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
this.onTouchMove = (event) => {
|
|
91
|
+
if (this.startY === null || event.touches.length !== 1) return;
|
|
92
|
+
|
|
93
|
+
const deltaY = event.touches[0].clientY - this.startY;
|
|
94
|
+
const deltaX = Math.abs(event.touches[0].clientX - this.startX);
|
|
95
|
+
if (deltaY <= 0 || deltaY <= deltaX) return;
|
|
96
|
+
|
|
97
|
+
this.currentDeltaY = deltaY;
|
|
98
|
+
this.element.style.transform = `translateY(${deltaY}px)`;
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
this.onTouchEnd = () => {
|
|
103
|
+
if (this.startY === null) return;
|
|
104
|
+
|
|
105
|
+
const shouldClose = this.currentDeltaY > 80;
|
|
106
|
+
this.startY = null;
|
|
107
|
+
this.startX = null;
|
|
108
|
+
|
|
109
|
+
if (shouldClose) {
|
|
110
|
+
this.close();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this.resetDragStyles(this.element);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
this.element.addEventListener("touchstart", this.onTouchStart, { passive: true });
|
|
118
|
+
this.element.addEventListener("touchmove", this.onTouchMove, { passive: false });
|
|
119
|
+
this.element.addEventListener("touchend", this.onTouchEnd);
|
|
120
|
+
this.element.addEventListener("touchcancel", this.onTouchEnd);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
detachSwipeGesture() {
|
|
124
|
+
if (this.element.id !== "roll-up") return;
|
|
125
|
+
if (!this.onTouchStart) return;
|
|
126
|
+
|
|
127
|
+
this.element.removeEventListener("touchstart", this.onTouchStart);
|
|
128
|
+
this.element.removeEventListener("touchmove", this.onTouchMove);
|
|
129
|
+
this.element.removeEventListener("touchend", this.onTouchEnd);
|
|
130
|
+
this.element.removeEventListener("touchcancel", this.onTouchEnd);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
resetDragStyles(element) {
|
|
134
|
+
element.style.transition = "";
|
|
135
|
+
element.style.transform = "";
|
|
42
136
|
}
|
|
43
137
|
|
|
44
138
|
createTable(items) {
|
|
@@ -19,7 +19,7 @@ class TailwindComponent < Tramway::BaseComponent
|
|
|
19
19
|
file_button: 'text-sm px-3 py-1',
|
|
20
20
|
submit_button: 'text-sm px-3 py-1',
|
|
21
21
|
tramway_select_input: 'text-sm px-2 py-1 h-10',
|
|
22
|
-
checkbox_input: 'h-4 w-4'
|
|
22
|
+
checkbox_input: 'min-h-4 min-w-4'
|
|
23
23
|
},
|
|
24
24
|
medium: {
|
|
25
25
|
text_input: 'text-base px-3 py-2',
|
|
@@ -27,7 +27,7 @@ class TailwindComponent < Tramway::BaseComponent
|
|
|
27
27
|
file_button: 'text-base px-4 py-2',
|
|
28
28
|
submit_button: 'text-base px-4 py-2',
|
|
29
29
|
tramway_select_input: 'text-base px-2 py-1 h-12',
|
|
30
|
-
checkbox_input: 'h-5 w-5'
|
|
30
|
+
checkbox_input: 'min-h-5 min-w-5'
|
|
31
31
|
},
|
|
32
32
|
large: {
|
|
33
33
|
text_input: 'text-xl px-4 py-3',
|
|
@@ -35,7 +35,7 @@ class TailwindComponent < Tramway::BaseComponent
|
|
|
35
35
|
file_button: 'text-xl px-5 py-3',
|
|
36
36
|
submit_button: 'text-xl px-5 py-3',
|
|
37
37
|
tramway_select_input: 'text-xl px-3 py-2 h-15',
|
|
38
|
-
checkbox_input: 'h-6 w-6'
|
|
38
|
+
checkbox_input: 'min-h-6 min-w-6'
|
|
39
39
|
}
|
|
40
40
|
}.freeze
|
|
41
41
|
|
|
@@ -12,13 +12,19 @@ module Tramway
|
|
|
12
12
|
|
|
13
13
|
def initialize(object_name, object, template, options)
|
|
14
14
|
@horizontal = options[:horizontal] || false
|
|
15
|
+
@remote = options[:remote_submit] || false
|
|
15
16
|
|
|
16
17
|
options.merge!(class: [options[:class], 'flex flex-row items-center gap-2'].compact.join(' ')) if @horizontal
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
@form_object_class = options[:form_object_class]
|
|
20
|
+
|
|
21
|
+
if form_object(object)
|
|
22
|
+
super(object_name, form_object(object), template, options)
|
|
23
|
+
else
|
|
24
|
+
super
|
|
25
|
+
end
|
|
19
26
|
|
|
20
27
|
@form_size = options[:size] || options['size'] || :medium
|
|
21
|
-
@form_object_class = options[:form_object_class]
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
def common_field(component_name, input_method, attribute, **options, &)
|
|
@@ -137,20 +143,55 @@ module Tramway
|
|
|
137
143
|
unbound_method.bind(self)
|
|
138
144
|
end
|
|
139
145
|
|
|
140
|
-
def form_object
|
|
141
|
-
|
|
146
|
+
def form_object(obj = nil)
|
|
147
|
+
return obj if obj.is_a?(Tramway::BaseForm)
|
|
148
|
+
return object if object.is_a?(Tramway::BaseForm)
|
|
149
|
+
|
|
150
|
+
@form_object_class&.new(obj || object)
|
|
142
151
|
end
|
|
143
152
|
|
|
144
153
|
def get_value(attribute, options)
|
|
145
|
-
options[:value]
|
|
154
|
+
return options[:value] if options.key?(:value)
|
|
155
|
+
|
|
156
|
+
form_obj = form_object
|
|
157
|
+
form_value = form_object_value(form_obj, attribute)
|
|
158
|
+
return form_value unless form_value.nil?
|
|
159
|
+
|
|
160
|
+
ensure_object_responds!(attribute, form_obj)
|
|
161
|
+
object_value(attribute)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def form_object_value(form_obj, attribute)
|
|
165
|
+
return if form_obj.blank?
|
|
166
|
+
|
|
167
|
+
form_obj.public_send(attribute)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def ensure_object_responds!(attribute, form_obj)
|
|
171
|
+
return unless object.present? && !object.respond_to?(attribute)
|
|
172
|
+
|
|
173
|
+
form_object_part = form_obj.present? ? "#{form_obj.class} or " : ''
|
|
174
|
+
message = "Neither form object nor object respond to #{attribute}. " \
|
|
175
|
+
"You should define #{attribute} method in #{form_object_part}#{object.class}"
|
|
176
|
+
|
|
177
|
+
raise ArgumentError, message
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def object_value(attribute)
|
|
181
|
+
return if object.blank?
|
|
182
|
+
|
|
183
|
+
object.public_send(attribute)
|
|
146
184
|
end
|
|
147
185
|
|
|
148
186
|
def default_options(attribute, options)
|
|
187
|
+
options.merge!(horizontal: true) if @horizontal
|
|
188
|
+
options.merge!(onchange: 'this.form.requestSubmit()') if @remote
|
|
189
|
+
|
|
149
190
|
{
|
|
150
191
|
attribute:,
|
|
151
192
|
label: label_build(attribute, options),
|
|
152
|
-
for: for_id(attribute),
|
|
153
|
-
options: options
|
|
193
|
+
for: options[:id].presence || for_id(attribute),
|
|
194
|
+
options: options,
|
|
154
195
|
size: form_size
|
|
155
196
|
}
|
|
156
197
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
.flex.items-
|
|
1
|
+
.flex.items-center.gap-2.cursor-pointer{ class: default_container_classes }
|
|
2
2
|
- classes = "#{size_class(:checkbox_input)} #{checkbox_base_classes}"
|
|
3
3
|
= @input.call @attribute, **@options.merge(class: classes)
|
|
4
4
|
- if @label
|
|
5
5
|
%div
|
|
6
|
-
= component('tramway/form/label', for: @for) do
|
|
6
|
+
= component('tramway/form/label', for: @for, options: { class: label_classes }) do
|
|
7
7
|
= @label
|
|
@@ -4,6 +4,20 @@ module Tramway
|
|
|
4
4
|
module Form
|
|
5
5
|
# Tailwind-styled checkbox field
|
|
6
6
|
class CheckboxComponent < TailwindComponent
|
|
7
|
+
def label_classes
|
|
8
|
+
default_classes = 'cursor-pointer mb-0'
|
|
9
|
+
|
|
10
|
+
case size
|
|
11
|
+
when :small
|
|
12
|
+
default_classes += ' text-sm'
|
|
13
|
+
when :medium
|
|
14
|
+
default_classes += ' text-base'
|
|
15
|
+
when :large
|
|
16
|
+
default_classes += ' text-lg'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
default_classes
|
|
20
|
+
end
|
|
7
21
|
end
|
|
8
22
|
end
|
|
9
23
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
%div{ class: default_container_classes }
|
|
2
2
|
- if @label
|
|
3
|
-
= component('tramway/form/label', for: @for) do
|
|
3
|
+
= component('tramway/form/label', for: @for, class: 'mb-0') do
|
|
4
4
|
= @label
|
|
5
5
|
- classes = "#{size_class(:text_input)} #{text_input_base_classes}"
|
|
6
6
|
= @input.call @attribute, **@options.merge(class: classes), value: @value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
%div{ class: default_container_classes }
|
|
2
2
|
- if @label
|
|
3
|
-
= component('tramway/form/label', for: @for) do
|
|
3
|
+
= component('tramway/form/label', for: @for, class: 'mb-0') do
|
|
4
4
|
= @label
|
|
5
5
|
- classes = "#{size_class(:text_input)} #{text_input_base_classes}"
|
|
6
6
|
= @input.call @attribute, **@options.merge(class: classes), value: @value
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
%label{ for: @for, class: form_label_classes }
|
|
1
|
+
%label{ for: @for, class: "#{form_label_classes} #{options[:class]}" }
|
|
2
2
|
= content
|
|
@@ -5,10 +5,11 @@ module Tramway
|
|
|
5
5
|
# Form label for all tailwind-styled forms
|
|
6
6
|
class LabelComponent < Tramway::BaseComponent
|
|
7
7
|
option :for
|
|
8
|
+
option :options, optional: true, default: -> { {} }
|
|
8
9
|
|
|
9
10
|
def form_label_classes
|
|
10
11
|
theme_classes(
|
|
11
|
-
classic: 'block
|
|
12
|
+
classic: 'block font-semibold text-white'
|
|
12
13
|
)
|
|
13
14
|
end
|
|
14
15
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
%div{ class: default_container_classes }
|
|
2
2
|
- if @label
|
|
3
|
-
= component('tramway/form/label', for: @for) do
|
|
3
|
+
= component('tramway/form/label', for: @for, class: 'mb-0') do
|
|
4
4
|
= @label
|
|
5
5
|
- classes = "#{size_class(:text_input)} #{text_input_base_classes}"
|
|
6
6
|
= @input.call @attribute, **@options.merge(class: classes), value: @value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
%div{ class: default_container_classes }
|
|
2
2
|
- if @label
|
|
3
|
-
= component('tramway/form/label', for: @for) do
|
|
3
|
+
= component('tramway/form/label', for: @for, class: 'mb-2') do
|
|
4
4
|
= @label
|
|
5
5
|
- classes = "#{size_class(:select_input)} #{select_base_classes}"
|
|
6
6
|
= @input.call(@attribute, @collection, { selected: @value }, @options.merge(class: classes))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
%div{ class: default_container_classes }
|
|
2
2
|
- if @label
|
|
3
|
-
= component('tramway/form/label', for: @for) do
|
|
3
|
+
= component('tramway/form/label', for: @for, class: 'mb-0') do
|
|
4
4
|
= @label
|
|
5
5
|
- classes = "#{size_class(:text_input)} #{text_input_base_classes}"
|
|
6
6
|
= @input.call @attribute, **@options.merge(class: classes), value: @value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
%div{ class: default_container_classes }
|
|
2
2
|
- if @label
|
|
3
|
-
= component('tramway/form/label', for: @for) do
|
|
3
|
+
= component('tramway/form/label', for: @for, class: 'mb-0') do
|
|
4
4
|
= @label
|
|
5
5
|
- classes = "#{size_class(:text_input)} #{text_input_base_classes}"
|
|
6
6
|
= @input.call @attribute, **@options.merge(class: classes), value: @value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
%div{ class: default_container_classes }
|
|
2
2
|
- if @label
|
|
3
|
-
= component('tramway/form/label', for: @for) do
|
|
3
|
+
= component('tramway/form/label', for: @for, class: 'mb-0') do
|
|
4
4
|
= @label
|
|
5
5
|
- classes = "#{size_class(:text_input)} #{text_input_base_classes}"
|
|
6
6
|
= @input.call @attribute, **@options.merge(class: classes), value: @value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.relative{ class: default_container_classes }
|
|
2
2
|
- if @label
|
|
3
|
-
= component('tramway/form/label', for: @for) do
|
|
3
|
+
= component('tramway/form/label', for: @for, class: 'mb-0') do
|
|
4
4
|
= @label
|
|
5
5
|
%div{ role: :combobox, data: tramway_select_hash, id: "#{@for}_tramway_select", class: tramway_select_classes }
|
|
6
6
|
- classes = "#{size_class(:tramway_select_input)} #{select_base_classes}"
|
|
@@ -36,42 +36,91 @@
|
|
|
36
36
|
|
|
37
37
|
:javascript
|
|
38
38
|
(() => {
|
|
39
|
-
const button = document.getElementById('mobile-menu-button');
|
|
40
|
-
const closeButton = document.getElementById('mobile-menu-close-button');
|
|
41
|
-
const menu = document.getElementById('mobile-menu');
|
|
42
39
|
const root = document.documentElement;
|
|
40
|
+
const MENU_HIDDEN_CLASS = 'hidden';
|
|
41
|
+
const MENU_OFFSCREEN_CLASS = '-translate-x-full';
|
|
42
|
+
const ROOT_LOCK_CLASS = 'overflow-hidden';
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const closeMenu = (menu, { hideImmediately = false } = {}) => {
|
|
45
|
+
if (!menu) {
|
|
46
|
+
root.classList.remove(ROOT_LOCK_CLASS);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
50
|
+
menu.classList.add(MENU_OFFSCREEN_CLASS);
|
|
51
|
+
root.classList.remove(ROOT_LOCK_CLASS);
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const isHiddenNow = menu.classList.contains('hidden');
|
|
56
|
-
|
|
57
|
-
if (isHiddenNow) {
|
|
58
|
-
menu.classList.remove('hidden');
|
|
59
|
-
requestAnimationFrame(() => {
|
|
60
|
-
menu.classList.remove('-translate-x-full');
|
|
61
|
-
root.classList.add('overflow-hidden');
|
|
62
|
-
});
|
|
63
|
-
} else {
|
|
64
|
-
menu.classList.add('-translate-x-full');
|
|
65
|
-
root.classList.remove('overflow-hidden');
|
|
66
|
-
|
|
67
|
-
menu.addEventListener(
|
|
68
|
-
'transitionend',
|
|
69
|
-
() => menu.classList.add('hidden'),
|
|
70
|
-
{ once: true }
|
|
71
|
-
);
|
|
53
|
+
if (hideImmediately) {
|
|
54
|
+
menu.classList.add(MENU_HIDDEN_CLASS);
|
|
55
|
+
return;
|
|
72
56
|
}
|
|
57
|
+
|
|
58
|
+
menu.addEventListener(
|
|
59
|
+
'transitionend',
|
|
60
|
+
() => menu.classList.add(MENU_HIDDEN_CLASS),
|
|
61
|
+
{ once: true }
|
|
62
|
+
);
|
|
73
63
|
};
|
|
74
64
|
|
|
75
|
-
|
|
76
|
-
|
|
65
|
+
const openMenu = (menu) => {
|
|
66
|
+
if (!menu) return;
|
|
67
|
+
|
|
68
|
+
menu.classList.remove(MENU_HIDDEN_CLASS);
|
|
69
|
+
requestAnimationFrame(() => {
|
|
70
|
+
menu.classList.remove(MENU_OFFSCREEN_CLASS);
|
|
71
|
+
root.classList.add(ROOT_LOCK_CLASS);
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const bindNavbar = () => {
|
|
76
|
+
const button = document.getElementById('mobile-menu-button');
|
|
77
|
+
const closeButton = document.getElementById('mobile-menu-close-button');
|
|
78
|
+
const menu = document.getElementById('mobile-menu');
|
|
79
|
+
|
|
80
|
+
if (!button || !closeButton || !menu || button.dataset.navbarBound === 'true') {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
button.dataset.navbarBound = 'true';
|
|
85
|
+
|
|
86
|
+
const toggleMenu = () => {
|
|
87
|
+
if (menu.classList.contains(MENU_HIDDEN_CLASS)) {
|
|
88
|
+
openMenu(menu);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
closeMenu(menu);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
button.addEventListener('click', toggleMenu);
|
|
96
|
+
closeButton.addEventListener('click', () => closeMenu(menu));
|
|
97
|
+
menu.querySelectorAll('a').forEach((link) => {
|
|
98
|
+
link.addEventListener('click', () => closeMenu(menu, { hideImmediately: true }));
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (window.__tramwayNavbarGlobalBound !== true) {
|
|
103
|
+
window.__tramwayNavbarGlobalBound = true;
|
|
104
|
+
|
|
105
|
+
document.addEventListener('turbo:before-cache', () => {
|
|
106
|
+
closeMenu(document.getElementById('mobile-menu'), { hideImmediately: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
document.addEventListener('turbo:load', () => {
|
|
110
|
+
const menu = document.getElementById('mobile-menu');
|
|
111
|
+
bindNavbar();
|
|
112
|
+
|
|
113
|
+
if (!menu || menu.classList.contains(MENU_HIDDEN_CLASS)) {
|
|
114
|
+
root.classList.remove(ROOT_LOCK_CLASS);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
window.addEventListener('resize', () => {
|
|
119
|
+
if (window.innerWidth >= 640) {
|
|
120
|
+
closeMenu(document.getElementById('mobile-menu'), { hideImmediately: true });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
bindNavbar();
|
|
77
126
|
})();
|
|
@@ -13,6 +13,22 @@
|
|
|
13
13
|
animation: roll-up 0.5s ease-in-out;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
@keyframes roll-down {
|
|
17
|
+
0% {
|
|
18
|
+
transform: translateY(0%);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
100% {
|
|
22
|
+
transform: translateY(100%);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.animate-roll-down {
|
|
27
|
+
animation: roll-down 0.5s ease-in-out;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#roll-up{ class: preview_classes, data: { controller: 'table-row-preview' } }
|
|
31
|
+
%button{ class: close_button_classes, "data-action" => "click->table-row-preview#close" }
|
|
18
32
|
✕
|
|
33
|
+
|
|
34
|
+
= content
|
|
@@ -7,7 +7,8 @@ module Tramway
|
|
|
7
7
|
class PreviewComponent < Tramway::BaseComponent
|
|
8
8
|
def preview_classes
|
|
9
9
|
theme_classes(
|
|
10
|
-
classic: 'fixed hidden
|
|
10
|
+
classic: 'fixed hidden md:hidden bottom-0 left-0 right-0 w-screen shadow-lg z-50 bg-gray-900 ' \
|
|
11
|
+
'animate-roll-up h-1/2 pt-4'
|
|
11
12
|
)
|
|
12
13
|
end
|
|
13
14
|
|
|
@@ -5,11 +5,18 @@
|
|
|
5
5
|
= value
|
|
6
6
|
|
|
7
7
|
- else
|
|
8
|
-
- cells =
|
|
8
|
+
- cells = visible_cells_from(content)
|
|
9
9
|
|
|
10
10
|
- if href.present?
|
|
11
11
|
= tag.a href:, class: [desktop_row_classes(cells.count), link_row_classes].join(' '), **default_attributes do
|
|
12
12
|
= content
|
|
13
13
|
- else
|
|
14
|
-
|
|
14
|
+
- if preview
|
|
15
|
+
= component 'tramway/table/row/preview' do
|
|
16
|
+
- cells.each_with_index do |cell, index|
|
|
17
|
+
- next if index.zero?
|
|
18
|
+
|
|
19
|
+
= cell.to_s.html_safe
|
|
20
|
+
|
|
21
|
+
= tag.div class: desktop_row_classes(cells.count), data: { action: "click->table-row-preview#toggle", controller: "table-row-preview" }, **default_attributes do
|
|
15
22
|
= content
|
|
@@ -6,6 +6,7 @@ module Tramway
|
|
|
6
6
|
class RowComponent < Tramway::BaseComponent
|
|
7
7
|
option :cells, optional: true, default: -> { [] }
|
|
8
8
|
option :href, optional: true
|
|
9
|
+
option :preview, optional: true, default: -> { true }
|
|
9
10
|
option :options, optional: true, default: -> { {} }
|
|
10
11
|
|
|
11
12
|
def default_attributes
|
|
@@ -57,6 +58,22 @@ module Tramway
|
|
|
57
58
|
|
|
58
59
|
private
|
|
59
60
|
|
|
61
|
+
def visible_cells_from(content)
|
|
62
|
+
fragment = Nokogiri::HTML.fragment(content)
|
|
63
|
+
parsed_cells = fragment.xpath(
|
|
64
|
+
'./*[@class and contains(concat(" ", normalize-space(@class), " "), " div-table-cell ")]'
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
parsed_cells.each { |cell| remove_hidden_class!(cell) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def remove_hidden_class!(node)
|
|
71
|
+
classes = node['class'].to_s.split
|
|
72
|
+
return if classes.empty?
|
|
73
|
+
|
|
74
|
+
node['class'] = classes.reject { |class_name| class_name == 'hidden' }.join(' ')
|
|
75
|
+
end
|
|
76
|
+
|
|
60
77
|
def ensure_view_context_accessor
|
|
61
78
|
return if view_context.respond_to?(:tramway_inside_cell=)
|
|
62
79
|
|
data/config/tailwind.config.js
CHANGED
|
@@ -118,6 +118,17 @@ module.exports = {
|
|
|
118
118
|
'first:block',
|
|
119
119
|
'rounded-t-xl',
|
|
120
120
|
|
|
121
|
+
// === Table row preview panel ===
|
|
122
|
+
'bottom-0',
|
|
123
|
+
'left-0',
|
|
124
|
+
'right-0',
|
|
125
|
+
'h-1/2',
|
|
126
|
+
'bg-gray-100',
|
|
127
|
+
'animate-roll-up',
|
|
128
|
+
'hover:text-gray-700',
|
|
129
|
+
'pt-4',
|
|
130
|
+
'md:hidden',
|
|
131
|
+
|
|
121
132
|
// === Title Component ===
|
|
122
133
|
'md:text-4xl',
|
|
123
134
|
|
|
@@ -333,6 +344,10 @@ module.exports = {
|
|
|
333
344
|
'bg-red-100',
|
|
334
345
|
'text-red-800',
|
|
335
346
|
'space-x-2',
|
|
347
|
+
'h-5',
|
|
348
|
+
'w-5',
|
|
349
|
+
'rounded-full',
|
|
350
|
+
'mb-0',
|
|
336
351
|
|
|
337
352
|
// === Tramway select dropdown positioning ===
|
|
338
353
|
'absolute',
|
|
@@ -368,12 +383,12 @@ module.exports = {
|
|
|
368
383
|
'h-15',
|
|
369
384
|
'h-12',
|
|
370
385
|
'h-10',
|
|
371
|
-
'w-2',
|
|
372
|
-
'h-2',
|
|
373
|
-
'w-4',
|
|
374
|
-
'h-4',
|
|
375
|
-
'w-6',
|
|
376
|
-
'h-6',
|
|
386
|
+
'min-w-2',
|
|
387
|
+
'min-h-2',
|
|
388
|
+
'min-w-4',
|
|
389
|
+
'min-h-4',
|
|
390
|
+
'min-w-6',
|
|
391
|
+
'min-h-6',
|
|
377
392
|
'px-2',
|
|
378
393
|
'py-1',
|
|
379
394
|
'flex-nowrap',
|
data/docs/AGENTS.md
CHANGED
|
@@ -149,6 +149,9 @@ Use Tramway Flash for user notifications.
|
|
|
149
149
|
### Rule 5
|
|
150
150
|
Use Tramway Table for tabular data display.
|
|
151
151
|
|
|
152
|
+
`tramway_row` supports `href:` for clickable rows and `preview:` for mobile row preview behavior. Keep `preview: true` as the
|
|
153
|
+
default unless the request explicitly needs preview disabled.
|
|
154
|
+
|
|
152
155
|
### Rule 6
|
|
153
156
|
Use Tramway Button for buttons. Always add a color of the button via `color:` or `type:` argument. `color:` argument support directs colors only: red, yellow, blue, etc. `type:` argument supports only lantern colors: will, hope, rage, etc.
|
|
154
157
|
|
|
@@ -173,6 +176,10 @@ Available `tramway_form_for` helpers:
|
|
|
173
176
|
- `tramway_select`
|
|
174
177
|
- `submit`
|
|
175
178
|
|
|
179
|
+
### Rule 7.1
|
|
180
|
+
Use `tramway_form_for(remote: true)` only when the form must submit asynchronously and update part of the current page (for example: modal forms, inline edits, or list updates without full page reload).
|
|
181
|
+
For standard create/update flows that redirect and show regular flash messages, keep it synchronous (do not set `remote: true`).
|
|
182
|
+
|
|
176
183
|
### Rule 8
|
|
177
184
|
Inherit all components from Tramway::BaseComponent
|
|
178
185
|
|
|
@@ -187,6 +194,69 @@ For live updates to a rendered `tramway_chat`, use `tramway_chat_append_message(
|
|
|
187
194
|
This method is included in all controllers and ActiveRecord models. `message_type` must be `:sent` or `:received`, otherwise
|
|
188
195
|
it raises `ArgumentError`. `chat_id` must match the stream id used in `tramway_chat`.
|
|
189
196
|
|
|
197
|
+
`message_form` object must be a `Tramway::BaseForm` or an inherited class object. It must contain `text` attribute.
|
|
198
|
+
`send_message_path` must be a `POST` route that receives data on sending messsage submition.
|
|
199
|
+
|
|
200
|
+
Here an example of usage:
|
|
201
|
+
|
|
202
|
+
*app/views/chats/show.html.haml*
|
|
203
|
+
```haml
|
|
204
|
+
= tramway_chat chat_id: @chat.id,
|
|
205
|
+
messages: @chat.messages_for_chat,
|
|
206
|
+
message_form: @message_form,
|
|
207
|
+
send_message_path: chats_messages_path
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
*app/decorators/chat_decorator.rb*
|
|
211
|
+
```ruby
|
|
212
|
+
class ChatDecorator < Tramway::BaseDecorator
|
|
213
|
+
def messages_for_chat
|
|
214
|
+
object.messages.map do |message|
|
|
215
|
+
{
|
|
216
|
+
id: message.id,
|
|
217
|
+
type: :sent, # or received
|
|
218
|
+
text: message.text,
|
|
219
|
+
sent_at: message.created_at,
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
*app/controllers/chats_controller.rb*
|
|
227
|
+
```ruby
|
|
228
|
+
def show
|
|
229
|
+
@chat = tramway_decorate Chat.find params[:id]
|
|
230
|
+
@message_form = tramway_form chat.messages.build
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**app/forms/message_form.rb**
|
|
235
|
+
```ruby
|
|
236
|
+
class Chats::MessageForm < Tramway::BaseForm
|
|
237
|
+
properties :text, :chat_id
|
|
238
|
+
end
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**config/routes.rb**
|
|
242
|
+
```ruby
|
|
243
|
+
resources :messages, only: :create
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**app/controllers/messages_controller.rb**
|
|
247
|
+
```ruby
|
|
248
|
+
def create
|
|
249
|
+
@message = tramway_form chat.creator.messages.build(chat:)
|
|
250
|
+
|
|
251
|
+
if @message.submit params[:message]
|
|
252
|
+
tramway_chat_append_message chat_id: @message.object.chat.id,
|
|
253
|
+
type: :sent,
|
|
254
|
+
text: @message.object.text,
|
|
255
|
+
sent_at: @message.object.created_at
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
```
|
|
259
|
+
|
|
190
260
|
### Rule 9
|
|
191
261
|
If page `create` or `update` is configured for an entity, use Tramway Form pattern for forms. Visible fields are configured via `form_fields` method.
|
|
192
262
|
|
|
@@ -501,6 +571,36 @@ and in a controller
|
|
|
501
571
|
end
|
|
502
572
|
```
|
|
503
573
|
|
|
574
|
+
Use dry-monads while call these services.
|
|
575
|
+
|
|
576
|
+
Instead of this
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
result = SomeService.call(args)
|
|
580
|
+
|
|
581
|
+
case result
|
|
582
|
+
when :success
|
|
583
|
+
# code
|
|
584
|
+
when :failure
|
|
585
|
+
# code
|
|
586
|
+
end
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Do this
|
|
590
|
+
|
|
591
|
+
```ruby
|
|
592
|
+
case SomeService.call(args)
|
|
593
|
+
in Success(result)
|
|
594
|
+
# use result
|
|
595
|
+
in Success(another_result)
|
|
596
|
+
# use another result
|
|
597
|
+
in Failure(reason_or_error)
|
|
598
|
+
# do stuff
|
|
599
|
+
end
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Success and Failure must describe all possible results of calling certain service.
|
|
603
|
+
|
|
504
604
|
### Rule 27
|
|
505
605
|
Don't create scopes for enumerated values, use `scope: :shallow` of the enumerize gem.
|
|
506
606
|
|
|
@@ -532,7 +632,15 @@ Always `tramway_decorate` and `tramway_form` for creating these types of objects
|
|
|
532
632
|
In Tramway Decorators, use `delegate_attributes` method instead of `delegate :something, to: :object`
|
|
533
633
|
|
|
534
634
|
### Rule 33
|
|
535
|
-
In case you want to use container on the page, use `tramway_container` helper instead of creating a component for that or using a plain div with Tailwind classes.
|
|
635
|
+
In case you want to use container on the page, use `tramway_container` helper instead of creating a component for that or using a plain div with Tailwind classes. In case you need to use container in layout view, use `tramway_main_container` for this. Here is example of using `tramway_main_container` inside application layout.
|
|
636
|
+
|
|
637
|
+
```
|
|
638
|
+
= tramway_main_container do
|
|
639
|
+
- if flash.any?
|
|
640
|
+
= tramway_flash text: flash[:notice].presence || flash[:alert],
|
|
641
|
+
type: flash[:notice].present? ? :will : :rage,
|
|
642
|
+
id: 'flash-container'
|
|
643
|
+
```
|
|
536
644
|
|
|
537
645
|
## Controller Patterns
|
|
538
646
|
|
|
@@ -52,10 +52,36 @@ module Tramway
|
|
|
52
52
|
@importmap_path ||= File.join(destination_root, 'config/importmap.rb')
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
def controllers_index_path
|
|
56
|
+
@controllers_index_path ||= File.join(destination_root, 'app/javascript/controllers/index.js')
|
|
57
|
+
end
|
|
58
|
+
|
|
55
59
|
def importmap_tramway_select_pin
|
|
56
60
|
'pin "@tramway/tramway-select", to: "tramway/tramway-select_controller.js"'
|
|
57
61
|
end
|
|
58
62
|
|
|
63
|
+
def importmap_table_row_preview_pin
|
|
64
|
+
'pin "@tramway/table-row-preview", to: "tramway/table_row_preview_controller.js"'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def importmap_tramway_pins
|
|
68
|
+
[importmap_tramway_select_pin, importmap_table_row_preview_pin]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def stimulus_controller_imports
|
|
72
|
+
[
|
|
73
|
+
'import { TramwaySelect } from "@tramway/tramway-select"',
|
|
74
|
+
'import { TableRowPreview } from "@tramway/table-row-preview"'
|
|
75
|
+
]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def stimulus_controller_registrations
|
|
79
|
+
[
|
|
80
|
+
"application.register('tramway-select', TramwaySelect)",
|
|
81
|
+
"application.register('table-row-preview', TableRowPreview)"
|
|
82
|
+
]
|
|
83
|
+
end
|
|
84
|
+
|
|
59
85
|
def agents_file_path
|
|
60
86
|
@agents_file_path ||= File.join(destination_root, 'AGENTS.md')
|
|
61
87
|
end
|
|
@@ -159,6 +185,43 @@ module Tramway
|
|
|
159
185
|
agents_template
|
|
160
186
|
)
|
|
161
187
|
end
|
|
188
|
+
|
|
189
|
+
# rubocop:disable Metrics/MethodLength
|
|
190
|
+
def append_missing_imports(content)
|
|
191
|
+
missing_imports = stimulus_controller_imports.reject { |line| content.include?(line) }
|
|
192
|
+
return content if missing_imports.empty?
|
|
193
|
+
|
|
194
|
+
import_lines = content.each_line.with_index.filter_map do |line, index|
|
|
195
|
+
index if line.lstrip.start_with?('import ')
|
|
196
|
+
end
|
|
197
|
+
insertion = "#{missing_imports.join("\n")}\n"
|
|
198
|
+
updated = content.dup
|
|
199
|
+
|
|
200
|
+
if import_lines.any?
|
|
201
|
+
insertion_index = updated.lines[0..import_lines.max].join.length
|
|
202
|
+
updated.insert(insertion_index, insertion)
|
|
203
|
+
else
|
|
204
|
+
updated.prepend(insertion)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
updated
|
|
208
|
+
end
|
|
209
|
+
# rubocop:enable Metrics/MethodLength
|
|
210
|
+
|
|
211
|
+
def append_missing_registrations(content)
|
|
212
|
+
missing_registrations = stimulus_controller_registrations.reject { |line| content.include?(line) }
|
|
213
|
+
return content if missing_registrations.empty?
|
|
214
|
+
|
|
215
|
+
insertion = "#{missing_registrations.join("\n")}\n"
|
|
216
|
+
export_match = /^(?:export\s*\{[^}]+\}\s*;?\s*)$/m.match(content)
|
|
217
|
+
|
|
218
|
+
return content.dup.insert(export_match.begin(0), insertion) if export_match
|
|
219
|
+
|
|
220
|
+
updated = content.dup
|
|
221
|
+
updated << "\n" unless updated.empty? || updated.end_with?("\n")
|
|
222
|
+
updated << insertion
|
|
223
|
+
updated
|
|
224
|
+
end
|
|
162
225
|
end
|
|
163
226
|
# rubocop:enable Metrics/ModuleLength
|
|
164
227
|
|
|
@@ -242,13 +305,25 @@ module Tramway
|
|
|
242
305
|
return unless File.exist?(importmap_path)
|
|
243
306
|
|
|
244
307
|
content = File.read(importmap_path)
|
|
245
|
-
|
|
308
|
+
missing_pins = importmap_tramway_pins.reject { |pin| content.include?(pin) }
|
|
309
|
+
return if missing_pins.empty?
|
|
246
310
|
|
|
247
311
|
File.open(importmap_path, 'a') do |file|
|
|
248
312
|
file.write("\n") unless content.empty? || content.end_with?("\n")
|
|
249
|
-
file.write("#{
|
|
313
|
+
file.write("#{missing_pins.join("\n")}\n")
|
|
250
314
|
end
|
|
251
315
|
end
|
|
316
|
+
|
|
317
|
+
def ensure_stimulus_controller_registration
|
|
318
|
+
return unless File.exist?(controllers_index_path)
|
|
319
|
+
|
|
320
|
+
content = File.read(controllers_index_path)
|
|
321
|
+
updated = append_missing_imports(content)
|
|
322
|
+
updated = append_missing_registrations(updated)
|
|
323
|
+
return if updated == content
|
|
324
|
+
|
|
325
|
+
File.write(controllers_index_path, updated)
|
|
326
|
+
end
|
|
252
327
|
end
|
|
253
328
|
end
|
|
254
329
|
end
|
|
@@ -11,12 +11,12 @@ module Tramway
|
|
|
11
11
|
def tramway_form_for(object, *, size: :medium, **options, &)
|
|
12
12
|
form_object_class = object.is_a?(Tramway::BaseForm) ? object.class : nil
|
|
13
13
|
|
|
14
|
-
form_for(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
)
|
|
14
|
+
form_for(object, *, **options.merge(
|
|
15
|
+
builder: Tramway::Form::Builder,
|
|
16
|
+
size: normalize_form_size(size),
|
|
17
|
+
form_object_class:,
|
|
18
|
+
remote_submit: options[:remote] || false
|
|
19
|
+
), &)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def tramway_table(**options, &)
|
|
@@ -34,6 +34,7 @@ module Tramway
|
|
|
34
34
|
component 'tramway/table/row',
|
|
35
35
|
cells: options.delete(:cells),
|
|
36
36
|
href: options.delete(:href),
|
|
37
|
+
preview: options.delete(:preview),
|
|
37
38
|
options:,
|
|
38
39
|
&
|
|
39
40
|
end
|
data/lib/tramway/utils/field.rb
CHANGED
|
@@ -42,9 +42,13 @@ module Tramway
|
|
|
42
42
|
|
|
43
43
|
def field_options(field_data)
|
|
44
44
|
if field_data.is_a?(Hash)
|
|
45
|
-
|
|
45
|
+
if field_data[:value].present?
|
|
46
|
+
value = field_data[:value]&.call(object)
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
field_data.merge(value:)
|
|
49
|
+
else
|
|
50
|
+
field_data
|
|
51
|
+
end.except(:type)
|
|
48
52
|
else
|
|
49
53
|
{}
|
|
50
54
|
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: 3.0.
|
|
4
|
+
version: 3.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- kalashnikovisme
|
|
@@ -320,7 +320,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
320
320
|
- !ruby/object:Gem::Version
|
|
321
321
|
version: '0'
|
|
322
322
|
requirements: []
|
|
323
|
-
rubygems_version: 4.0.
|
|
323
|
+
rubygems_version: 4.0.6
|
|
324
324
|
specification_version: 4
|
|
325
325
|
summary: Tramway Rails Engine
|
|
326
326
|
test_files: []
|