tramway 3.0.2 → 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 +14 -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/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 +17 -6
- data/docs/AGENTS.md +96 -0
- data/lib/generators/tramway/install/install_generator.rb +77 -2
- data/lib/tramway/helpers/views_helper.rb +1 -0
- 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
|
|
@@ -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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
%div{ class: default_container_classes }
|
|
2
2
|
- if @label
|
|
3
|
-
= component('tramway/form/label', for: @for, class: 'mb-
|
|
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))
|
|
@@ -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
|
|
|
@@ -372,12 +383,12 @@ module.exports = {
|
|
|
372
383
|
'h-15',
|
|
373
384
|
'h-12',
|
|
374
385
|
'h-10',
|
|
375
|
-
'w-2',
|
|
376
|
-
'h-2',
|
|
377
|
-
'w-4',
|
|
378
|
-
'h-4',
|
|
379
|
-
'w-6',
|
|
380
|
-
'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',
|
|
381
392
|
'px-2',
|
|
382
393
|
'py-1',
|
|
383
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
|
|
|
@@ -191,6 +194,69 @@ For live updates to a rendered `tramway_chat`, use `tramway_chat_append_message(
|
|
|
191
194
|
This method is included in all controllers and ActiveRecord models. `message_type` must be `:sent` or `:received`, otherwise
|
|
192
195
|
it raises `ArgumentError`. `chat_id` must match the stream id used in `tramway_chat`.
|
|
193
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
|
+
|
|
194
260
|
### Rule 9
|
|
195
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.
|
|
196
262
|
|
|
@@ -505,6 +571,36 @@ and in a controller
|
|
|
505
571
|
end
|
|
506
572
|
```
|
|
507
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
|
+
|
|
508
604
|
### Rule 27
|
|
509
605
|
Don't create scopes for enumerated values, use `scope: :shallow` of the enumerize gem.
|
|
510
606
|
|
|
@@ -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
|
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: []
|