tramway 3.1 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +36 -5
- data/app/assets/javascripts/tramway/tramway.js +418 -0
- data/app/components/tramway/badge_component.rb +42 -6
- data/app/components/tramway/button_component.html.haml +30 -23
- data/app/components/tramway/button_component.rb +31 -1
- data/app/components/tramway/form/builder.rb +1 -1
- data/app/components/tramway/tooltip_component.html.haml +12 -0
- data/app/components/tramway/tooltip_component.rb +93 -0
- data/app/views/tramway/entities/_form.html.haml +1 -1
- data/config/tailwind.config.js +41 -1
- data/lib/generators/tramway/install/install_generator.rb +41 -29
- data/lib/tramway/engine.rb +3 -1
- data/lib/tramway/helpers/views_helper.rb +5 -1
- data/lib/tramway/version.rb +1 -1
- metadata +5 -5
- data/app/assets/javascripts/tramway/table_row_preview_controller.js +0 -175
- data/app/assets/javascripts/tramway/tramway-select_controller.js +0 -198
- data/app/assets/javascripts/tramway/ui_checkbox_controller.js +0 -36
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 000e5072249d738796fc44b63dccf1f228bd028eec1b63526450e2c69f5ddea5
|
|
4
|
+
data.tar.gz: 97b97098ee9f835a6372411cd7dee96ef5e16011f3262479c43e72077a07ec87
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e51c09e468e483f088b6d148804b423e1a092cc6887b0e83b51a13ae8eec458a0abd527d625a40cf963b9e2c98578012c5fddc09c1e9b5278dd1586a2b13d5b0
|
|
7
|
+
data.tar.gz: 51eaf91dfb27013da6fd432166704dbf7a919c7f1f1a5e86e0ce54b4e76fbb755b72d91069e5546d57222249ae2433dffd432585be913dcf28b3c56bfee65f16
|
data/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Codex instruction that points agents to the Tramway skill for Tramway-native cod
|
|
|
17
17
|
* [Tramway Form](https://github.com/Purple-Magic/tramway#tramway-form)
|
|
18
18
|
* [Tramway Navbar](https://github.com/Purple-Magic/tramway#tramway-navbar)
|
|
19
19
|
* [Tramway Flash](https://github.com/Purple-Magic/tramway#tramway-flash)
|
|
20
|
+
* [Tramway Tooltip](https://github.com/Purple-Magic/tramway#tramway-tooltip)
|
|
20
21
|
* [Tramway Chat](https://github.com/Purple-Magic/tramway#tramway-chat)
|
|
21
22
|
* [Tramway Table Component](https://github.com/Purple-Magic/tramway#tramway-table-component)
|
|
22
23
|
* [Tailwind-styled forms](https://github.com/Purple-Magic/tramway#tailwind-styled-forms)
|
|
@@ -891,6 +892,29 @@ they will be merged into the flash container.
|
|
|
891
892
|
Use the `type` argument is compatible to [Lantern Color Palette](https://github.com/TrinityMonsters/tramway/blob/main/README.md#lantern-color-palette) or provide a `color:` keyword to set
|
|
892
893
|
the semantic accent explicitly.
|
|
893
894
|
|
|
895
|
+
### Tramway Tooltip
|
|
896
|
+
|
|
897
|
+
`tramway_tooltip` renders a dark tooltip around block content with a default minimum width of `min-w-40` and maximum width of `max-w-sm`. Pass `text:` for the tooltip body and `event:` to
|
|
898
|
+
choose when it appears. Supported events are `:hover` and `:onclick`; hover is the default.
|
|
899
|
+
|
|
900
|
+
```haml
|
|
901
|
+
= tramway_tooltip text: 'Shown on hover' do
|
|
902
|
+
%span Help
|
|
903
|
+
|
|
904
|
+
= tramway_tooltip text: 'Shown after click', event: :onclick do
|
|
905
|
+
%button More info
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
```erb
|
|
909
|
+
<%= tramway_tooltip text: 'Shown on hover' do %>
|
|
910
|
+
<span>Help</span>
|
|
911
|
+
<% end %>
|
|
912
|
+
|
|
913
|
+
<%= tramway_tooltip text: 'Shown after click', event: :onclick do %>
|
|
914
|
+
<button>More info</button>
|
|
915
|
+
<% end %>
|
|
916
|
+
```
|
|
917
|
+
|
|
894
918
|
### Tramway Chat
|
|
895
919
|
|
|
896
920
|
`tramway_chat` renders the chat experience bundled with Tramway. Provide a chat ID, a list of message hashes, and the URL
|
|
@@ -1130,9 +1154,16 @@ Example 3: rendering button
|
|
|
1130
1154
|
<%= tramway_button path: '/projects', text: 'Projects', size: :small %>
|
|
1131
1155
|
```
|
|
1132
1156
|
|
|
1133
|
-
|
|
1134
|
-
`:
|
|
1135
|
-
|
|
1157
|
+
Use `tooltip:` to show Tramway's tooltip from the rendered button. The tooltip accepts the same `event:` values as
|
|
1158
|
+
`tramway_tooltip`: `:hover` or `:onclick`.
|
|
1159
|
+
|
|
1160
|
+
```erb
|
|
1161
|
+
<%= tramway_button path: '/projects', text: 'Projects', tooltip: { text: 'Open projects', event: :hover } %>
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
* `tramway_badge` renders a dark shadcn-style Tailwind badge with the provided `text`. Pass a semantic `type` (for example,
|
|
1165
|
+
`:success` or `:danger`) to use the built-in color mappings, or supply a custom Tailwind color family with `color:`. When
|
|
1166
|
+
you opt into a custom color, ensure the corresponding accent utilities are available in your Tailwind safelist.
|
|
1136
1167
|
|
|
1137
1168
|
```erb
|
|
1138
1169
|
<%= tramway_badge text: 'Active', type: :success %>
|
|
@@ -1273,14 +1304,14 @@ Example for [importmap-rails](https://github.com/rails/importmap-rails) config
|
|
|
1273
1304
|
|
|
1274
1305
|
*config/importmap.rb*
|
|
1275
1306
|
```ruby
|
|
1276
|
-
pin '@tramway/tramway
|
|
1307
|
+
pin '@tramway/tramway', to: 'tramway/tramway.js'
|
|
1277
1308
|
```
|
|
1278
1309
|
|
|
1279
1310
|
*app/javascript/controllers/index.js*
|
|
1280
1311
|
```js
|
|
1281
1312
|
import { application } from "controllers/application"
|
|
1282
1313
|
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
|
|
1283
|
-
import { TramwaySelect } from "@tramway/tramway
|
|
1314
|
+
import { TramwaySelect } from "@tramway/tramway" // importing TramwaySelect controller class
|
|
1284
1315
|
eagerLoadControllersFrom("controllers", application)
|
|
1285
1316
|
|
|
1286
1317
|
application.register('tramway-select', TramwaySelect) // register TramwaySelect controller class as `tramway-select` stimulus controller
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
class TramwaySelect extends Controller {
|
|
4
|
+
static targets = ["dropdown", "showSelectedArea", "hiddenInput", "caretDown", "caretUp"]
|
|
5
|
+
|
|
6
|
+
static values = {
|
|
7
|
+
items: Array,
|
|
8
|
+
dropdownContainer: String,
|
|
9
|
+
itemContainer: String,
|
|
10
|
+
selectedItemTemplate: String,
|
|
11
|
+
dropdownState: String,
|
|
12
|
+
selectedItems: Array,
|
|
13
|
+
placeholder: String,
|
|
14
|
+
selectAsInput: String,
|
|
15
|
+
value: Array,
|
|
16
|
+
onChange: String,
|
|
17
|
+
multiple: Boolean,
|
|
18
|
+
autocomplete: Boolean,
|
|
19
|
+
autocompleteInput: String
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
connect() {
|
|
23
|
+
this.dropdownState = 'closed';
|
|
24
|
+
|
|
25
|
+
this.items = JSON.parse(this.element.dataset.items).map((item, index) => {
|
|
26
|
+
return {
|
|
27
|
+
index,
|
|
28
|
+
text: item.text,
|
|
29
|
+
value: item.value.toString(),
|
|
30
|
+
selected: false
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const initialValues = this.element.dataset.value === undefined ? [] : JSON.parse(this.element.dataset.value);
|
|
35
|
+
|
|
36
|
+
initialValues.map((value) => {
|
|
37
|
+
const itemIndex = this.items.findIndex(x => x.value.toString() === value.toString());
|
|
38
|
+
this.items[itemIndex].selected = true;
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
this.selectedItems = this.items.filter(item => item.selected);
|
|
42
|
+
|
|
43
|
+
this.renderSelectedItems();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
renderSelectedItems() {
|
|
47
|
+
const allItems = this.fillTemplate(this.element.dataset.selectedItemTemplate, this.selectedItems)
|
|
48
|
+
|
|
49
|
+
let content = allItems;
|
|
50
|
+
|
|
51
|
+
if (this.autocomplete() && this.selectedItems.length === 0) {
|
|
52
|
+
content += this.element.dataset.autocompleteInput;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.showSelectedAreaTarget.innerHTML = content;
|
|
56
|
+
this.showSelectedAreaTarget.insertAdjacentHTML("beforeEnd", this.input());
|
|
57
|
+
this.updateInputOptions();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fillTemplate(template, items) {
|
|
61
|
+
return items.map((item) => {
|
|
62
|
+
return template.replace(/{{text}}/g, item.text).replace(/{{value}}/g, item.value)
|
|
63
|
+
}).join('')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
closeOnClickOutside(event) {
|
|
67
|
+
if (this.dropdownState === 'open' && !this.element.contains(event.target)) {
|
|
68
|
+
this.closeDropdown();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
toggleDropdown() {
|
|
73
|
+
if (this.dropdownState === 'closed') {
|
|
74
|
+
this.openDropdown();
|
|
75
|
+
} else {
|
|
76
|
+
this.closeDropdown();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
rerenderItems() {
|
|
81
|
+
this.closeDropdown();
|
|
82
|
+
this.openDropdown();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
openDropdown() {
|
|
86
|
+
this.dropdownState = 'open';
|
|
87
|
+
this.dropdownTarget.insertAdjacentHTML("afterend", this.template);
|
|
88
|
+
|
|
89
|
+
if (this.dropdown()) {
|
|
90
|
+
this.dropdown().addEventListener('click', event => event.stopPropagation());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.caretDownTarget.classList.add('hidden');
|
|
94
|
+
this.caretUpTarget.classList.remove('hidden');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
dropdown() {
|
|
98
|
+
return this.element.querySelector('#dropdown');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
closeDropdown() {
|
|
102
|
+
this.dropdownState = 'closed';
|
|
103
|
+
|
|
104
|
+
if (this.dropdown()) {
|
|
105
|
+
this.dropdown().remove();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const onChange = this.element.dataset.onChange;
|
|
109
|
+
|
|
110
|
+
if (onChange) {
|
|
111
|
+
const [controllerName, actionName] = onChange.split('#');
|
|
112
|
+
const controller = this.application.controllers.find(controller => controller.identifier === controllerName)
|
|
113
|
+
|
|
114
|
+
if (controller) {
|
|
115
|
+
if (typeof controller[actionName] === 'function') {
|
|
116
|
+
controller[actionName]({ target: this.element });
|
|
117
|
+
} else {
|
|
118
|
+
alert(`Action not found: ${actionName}`); // eslint-disable-line no-undef
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
alert(`Controller not found: ${controllerName}`); // eslint-disable-line no-undef
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.caretDownTarget.classList.remove('hidden');
|
|
126
|
+
this.caretUpTarget.classList.add('hidden');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get template() {
|
|
130
|
+
return this.element.dataset.dropdownContainer.replace(
|
|
131
|
+
/{{content}}/g,
|
|
132
|
+
this.fillTemplate(this.element.dataset.itemContainer, this.items.filter(item => !item.selected))
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
toggleItem({ currentTarget }) {
|
|
137
|
+
const itemIndex = this.items.findIndex(x => x.value === currentTarget.dataset.value);
|
|
138
|
+
const itemSelectedIndex = this.selectedItems.findIndex(x => x.value === currentTarget.dataset.value);
|
|
139
|
+
|
|
140
|
+
if (!this.multiple()) {
|
|
141
|
+
this.selectedItems = [];
|
|
142
|
+
this.items.forEach(item => item.selected = false);
|
|
143
|
+
this.closeDropdown()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (itemSelectedIndex !== -1) {
|
|
147
|
+
this.selectedItems = this.selectedItems.filter((_, index) => index !== itemSelectedIndex);
|
|
148
|
+
this.items[itemIndex].selected = false;
|
|
149
|
+
} else {
|
|
150
|
+
this.selectedItems.push(this.items[itemIndex]);
|
|
151
|
+
this.items[itemIndex].selected = true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.renderSelectedItems();
|
|
155
|
+
|
|
156
|
+
if (this.multiple()) {
|
|
157
|
+
this.rerenderItems();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
input() {
|
|
162
|
+
const placeholder = this.selectedItems.length > 0 ? '' : this.element.dataset.placeholder;
|
|
163
|
+
return this.element.dataset.selectAsInput.replace(/{{placeholder}}/g, placeholder);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
updateInputOptions() {
|
|
167
|
+
this.hiddenInputTarget.innerHTML = '';
|
|
168
|
+
this.selectedItems.forEach(selected => {
|
|
169
|
+
const option = document.createElement("option");
|
|
170
|
+
option.text = selected.text;
|
|
171
|
+
option.value = selected.value;
|
|
172
|
+
option.setAttribute("selected", true);
|
|
173
|
+
this.hiddenInputTarget.append(option);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this.hiddenInputTarget.value = this.selectedItems.map(item => item.value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
multiple() {
|
|
180
|
+
return this.element.dataset.multiple == 'true';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
autocomplete() {
|
|
184
|
+
return this.element.dataset.autocomplete == 'true';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
search(event) {
|
|
188
|
+
const searchTerm = event.target.value.toLowerCase();
|
|
189
|
+
const filteredItems = this.items.filter(item => item.text.toLowerCase().includes(searchTerm) && !item.selected);
|
|
190
|
+
const dropdown = this.dropdown();
|
|
191
|
+
|
|
192
|
+
if (dropdown) {
|
|
193
|
+
dropdown.innerHTML = this.fillTemplate(this.element.dataset.itemContainer, filteredItems);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
class TableRowPreview extends Controller {
|
|
199
|
+
connect() {
|
|
200
|
+
this.items = JSON.parse(this.element.dataset.items || '{}');
|
|
201
|
+
this.attachSwipeGesture();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
disconnect() {
|
|
205
|
+
this.detachSwipeGesture();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
toggle() {
|
|
209
|
+
const rollUp = this.rollUpElement();
|
|
210
|
+
if (!rollUp) return;
|
|
211
|
+
|
|
212
|
+
rollUp.classList.remove("animate-roll-down");
|
|
213
|
+
rollUp.classList.add("animate-roll-up");
|
|
214
|
+
|
|
215
|
+
if (Object.keys(this.items).length === 0) {
|
|
216
|
+
rollUp.classList.remove("hidden");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const existingTable = rollUp.querySelector(".div-table");
|
|
221
|
+
if (existingTable) {
|
|
222
|
+
existingTable.remove();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const existingTitle = rollUp.querySelector("h3");
|
|
226
|
+
if (existingTitle) {
|
|
227
|
+
existingTitle.remove();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const titleText = document.createElement("h3");
|
|
231
|
+
|
|
232
|
+
titleText.classList.add("text-xl");
|
|
233
|
+
titleText.classList.add("text-white");
|
|
234
|
+
titleText.classList.add("py-4");
|
|
235
|
+
titleText.classList.add("px-4");
|
|
236
|
+
titleText.textContent = Object.values(this.items)[0];
|
|
237
|
+
|
|
238
|
+
const table = this.createTable(this.items);
|
|
239
|
+
|
|
240
|
+
rollUp.insertAdjacentElement('afterbegin', table);
|
|
241
|
+
rollUp.insertAdjacentElement('afterbegin', titleText);
|
|
242
|
+
|
|
243
|
+
rollUp.classList.remove("hidden");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
close() {
|
|
247
|
+
const rollUp = this.rollUpElement();
|
|
248
|
+
if (!rollUp) return;
|
|
249
|
+
|
|
250
|
+
this.resetDragStyles(rollUp);
|
|
251
|
+
rollUp.classList.remove("animate-roll-up");
|
|
252
|
+
rollUp.classList.add("animate-roll-down");
|
|
253
|
+
|
|
254
|
+
rollUp.addEventListener("animationend", () => {
|
|
255
|
+
rollUp.classList.add("hidden");
|
|
256
|
+
rollUp.classList.remove("animate-roll-down");
|
|
257
|
+
rollUp.classList.add("animate-roll-up");
|
|
258
|
+
}, { once: true });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
rollUpElement() {
|
|
262
|
+
if (this.element.id === "roll-up") return this.element;
|
|
263
|
+
|
|
264
|
+
return this.element.previousElementSibling || document.getElementById("roll-up");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
attachSwipeGesture() {
|
|
268
|
+
if (this.element.id !== "roll-up") return;
|
|
269
|
+
|
|
270
|
+
this.startY = null;
|
|
271
|
+
this.startX = null;
|
|
272
|
+
this.currentDeltaY = 0;
|
|
273
|
+
|
|
274
|
+
this.onTouchStart = (event) => {
|
|
275
|
+
if (this.element.classList.contains("hidden")) return;
|
|
276
|
+
if (event.touches.length !== 1) return;
|
|
277
|
+
|
|
278
|
+
this.startY = event.touches[0].clientY;
|
|
279
|
+
this.startX = event.touches[0].clientX;
|
|
280
|
+
this.currentDeltaY = 0;
|
|
281
|
+
this.element.style.transition = "none";
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
this.onTouchMove = (event) => {
|
|
285
|
+
if (this.startY === null || event.touches.length !== 1) return;
|
|
286
|
+
|
|
287
|
+
const deltaY = event.touches[0].clientY - this.startY;
|
|
288
|
+
const deltaX = Math.abs(event.touches[0].clientX - this.startX);
|
|
289
|
+
if (deltaY <= 0 || deltaY <= deltaX) return;
|
|
290
|
+
|
|
291
|
+
this.currentDeltaY = deltaY;
|
|
292
|
+
this.element.style.transform = `translateY(${deltaY}px)`;
|
|
293
|
+
event.preventDefault();
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
this.onTouchEnd = () => {
|
|
297
|
+
if (this.startY === null) return;
|
|
298
|
+
|
|
299
|
+
const shouldClose = this.currentDeltaY > 80;
|
|
300
|
+
this.startY = null;
|
|
301
|
+
this.startX = null;
|
|
302
|
+
|
|
303
|
+
if (shouldClose) {
|
|
304
|
+
this.close();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.resetDragStyles(this.element);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
this.element.addEventListener("touchstart", this.onTouchStart, { passive: true });
|
|
312
|
+
this.element.addEventListener("touchmove", this.onTouchMove, { passive: false });
|
|
313
|
+
this.element.addEventListener("touchend", this.onTouchEnd);
|
|
314
|
+
this.element.addEventListener("touchcancel", this.onTouchEnd);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
detachSwipeGesture() {
|
|
318
|
+
if (this.element.id !== "roll-up") return;
|
|
319
|
+
if (!this.onTouchStart) return;
|
|
320
|
+
|
|
321
|
+
this.element.removeEventListener("touchstart", this.onTouchStart);
|
|
322
|
+
this.element.removeEventListener("touchmove", this.onTouchMove);
|
|
323
|
+
this.element.removeEventListener("touchend", this.onTouchEnd);
|
|
324
|
+
this.element.removeEventListener("touchcancel", this.onTouchEnd);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
resetDragStyles(element) {
|
|
328
|
+
element.style.transition = "";
|
|
329
|
+
element.style.transform = "";
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
createTable(items) {
|
|
333
|
+
const table = document.createElement("div");
|
|
334
|
+
table.classList.add("div-table");
|
|
335
|
+
table.classList.add("text-white");
|
|
336
|
+
table.classList.add("px-2");
|
|
337
|
+
|
|
338
|
+
Object.entries(items).forEach(([key, value]) => {
|
|
339
|
+
const rows = this.createTableRow(key, value);
|
|
340
|
+
|
|
341
|
+
rows.forEach((row) => table.appendChild(row));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return table;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
createTableRow(key, value) {
|
|
348
|
+
const keyRow = document.createElement("div");
|
|
349
|
+
keyRow.classList.add("div-table-row");
|
|
350
|
+
keyRow.classList.add("bg-gray-700");
|
|
351
|
+
keyRow.classList.add("text-white");
|
|
352
|
+
keyRow.classList.add("px-2");
|
|
353
|
+
keyRow.classList.add("py-1");
|
|
354
|
+
keyRow.classList.add("text-xs");
|
|
355
|
+
keyRow.classList.add("font-semibold");
|
|
356
|
+
keyRow.textContent = key;
|
|
357
|
+
|
|
358
|
+
const valueRow = document.createElement("div");
|
|
359
|
+
valueRow.classList.add("div-table-row");
|
|
360
|
+
valueRow.classList.add("bg-gray-800");
|
|
361
|
+
valueRow.classList.add("px-2");
|
|
362
|
+
valueRow.classList.add("py-2");
|
|
363
|
+
valueRow.textContent = value;
|
|
364
|
+
|
|
365
|
+
return [keyRow, valueRow];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
class UiCheckbox extends Controller {
|
|
370
|
+
static targets = ["input", "button", "indicator"]
|
|
371
|
+
|
|
372
|
+
connect() {
|
|
373
|
+
this.sync()
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
toggle(event) {
|
|
377
|
+
event.preventDefault()
|
|
378
|
+
|
|
379
|
+
if (this.inputTarget.disabled) return
|
|
380
|
+
|
|
381
|
+
this.inputTarget.click()
|
|
382
|
+
this.sync()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
sync() {
|
|
386
|
+
const checked = this.inputTarget.checked
|
|
387
|
+
const state = checked ? "checked" : "unchecked"
|
|
388
|
+
|
|
389
|
+
this.buttonTarget.setAttribute("aria-checked", checked.toString())
|
|
390
|
+
this.buttonTarget.dataset.state = state
|
|
391
|
+
this.buttonTarget.classList.toggle("border-zinc-50", checked)
|
|
392
|
+
this.buttonTarget.classList.toggle("bg-zinc-50", checked)
|
|
393
|
+
this.buttonTarget.classList.toggle("text-zinc-950", checked)
|
|
394
|
+
this.buttonTarget.classList.toggle("border-zinc-800", !checked)
|
|
395
|
+
this.buttonTarget.classList.toggle("bg-zinc-950", !checked)
|
|
396
|
+
this.buttonTarget.classList.toggle("text-zinc-50", !checked)
|
|
397
|
+
this.indicatorTarget.classList.toggle("hidden", !checked)
|
|
398
|
+
this.buttonTarget.toggleAttribute("disabled", this.inputTarget.disabled)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
class Tooltip extends Controller {
|
|
403
|
+
static targets = ["panel"]
|
|
404
|
+
|
|
405
|
+
toggle(event) {
|
|
406
|
+
event.stopPropagation()
|
|
407
|
+
|
|
408
|
+
this.panelTarget.classList.toggle("hidden")
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
closeOnClickOutside(event) {
|
|
412
|
+
if (this.element.contains(event.target)) return
|
|
413
|
+
|
|
414
|
+
this.panelTarget.classList.add("hidden")
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export { TramwaySelect, TableRowPreview, UiCheckbox, Tooltip }
|
|
@@ -4,6 +4,11 @@ module Tramway
|
|
|
4
4
|
# Default Tramway badge
|
|
5
5
|
#
|
|
6
6
|
class BadgeComponent < Tramway::BaseComponent
|
|
7
|
+
DEFAULT_BADGE_CLASSES = %w[
|
|
8
|
+
inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors
|
|
9
|
+
focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 focus:ring-offset-zinc-950
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
7
12
|
option :text
|
|
8
13
|
option :type, optional: true
|
|
9
14
|
option :color, optional: true
|
|
@@ -11,12 +16,43 @@ module Tramway
|
|
|
11
16
|
include Tramway::ColorsMethods
|
|
12
17
|
|
|
13
18
|
def classes
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
(DEFAULT_BADGE_CLASSES + color_classes).join(' ')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def color_classes
|
|
23
|
+
return accent_color_classes if color.present?
|
|
24
|
+
|
|
25
|
+
mapped_color_classes || accent_color_classes
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def mapped_color_classes
|
|
29
|
+
{
|
|
30
|
+
default: default_color_classes,
|
|
31
|
+
life: default_color_classes,
|
|
32
|
+
secondary: secondary_color_classes,
|
|
33
|
+
inverse: inverse_color_classes
|
|
34
|
+
}[normalized_type]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def default_color_classes
|
|
38
|
+
%w[border-transparent bg-zinc-50 text-zinc-950 shadow hover:bg-zinc-200]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def secondary_color_classes
|
|
42
|
+
%w[border-transparent bg-zinc-800 text-zinc-50 shadow hover:bg-zinc-800/80]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def inverse_color_classes
|
|
46
|
+
%w[border-zinc-800 bg-zinc-950 text-zinc-50 shadow hover:bg-zinc-900]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def accent_color_classes
|
|
50
|
+
[
|
|
51
|
+
"border-#{resolved_color}-800",
|
|
52
|
+
"bg-#{resolved_color}-900/30",
|
|
53
|
+
"text-#{resolved_color}-400",
|
|
54
|
+
"hover:bg-#{resolved_color}-900"
|
|
55
|
+
]
|
|
20
56
|
end
|
|
21
57
|
end
|
|
22
58
|
end
|
|
@@ -1,27 +1,34 @@
|
|
|
1
|
-
-
|
|
2
|
-
- if
|
|
3
|
-
|
|
4
|
-
- else
|
|
5
|
-
- case @tag
|
|
6
|
-
- when :a
|
|
7
|
-
= link_to text, path, class: classes, **render_options
|
|
8
|
-
- when :button
|
|
9
|
-
%button{ class: classes, **render_options }
|
|
10
|
-
= text
|
|
11
|
-
- when :form
|
|
1
|
+
- button_markup = capture do
|
|
2
|
+
- if text.present?
|
|
3
|
+
- if stop_cell_propagation?
|
|
12
4
|
= helpers.button_to text, path, method:, form: form_options, class: classes, **render_options
|
|
13
|
-
- else
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
- else
|
|
6
|
+
- case @tag
|
|
7
|
+
- when :a
|
|
8
|
+
= link_to text, path, class: classes, **render_options
|
|
9
|
+
- when :button
|
|
10
|
+
%button{ class: classes, **render_options }
|
|
11
|
+
= text
|
|
12
|
+
- when :form
|
|
13
|
+
= helpers.button_to text, path, method:, form: form_options, class: classes, **render_options
|
|
17
14
|
- else
|
|
18
|
-
-
|
|
19
|
-
- when :a
|
|
20
|
-
= link_to path, class: classes, **render_options do
|
|
21
|
-
= content
|
|
22
|
-
- when :button
|
|
23
|
-
%button{ class: classes, **render_options }
|
|
24
|
-
= content
|
|
25
|
-
- when :form
|
|
15
|
+
- if stop_cell_propagation?
|
|
26
16
|
= helpers.button_to path, method:, class: classes, form: form_options, **render_options do
|
|
27
17
|
= content
|
|
18
|
+
- else
|
|
19
|
+
- case @tag
|
|
20
|
+
- when :a
|
|
21
|
+
= link_to path, class: classes, **render_options do
|
|
22
|
+
= content
|
|
23
|
+
- when :button
|
|
24
|
+
%button{ class: classes, **render_options }
|
|
25
|
+
= content
|
|
26
|
+
- when :form
|
|
27
|
+
= helpers.button_to path, method:, class: classes, form: form_options, **render_options do
|
|
28
|
+
= content
|
|
29
|
+
|
|
30
|
+
- if tooltip.present?
|
|
31
|
+
= render Tramway::TooltipComponent.new(**tooltip_options) do
|
|
32
|
+
= button_markup
|
|
33
|
+
- else
|
|
34
|
+
= button_markup
|
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Tramway
|
|
4
|
+
# Tooltip options handling for Tramway button
|
|
5
|
+
module ButtonComponentTooltip
|
|
6
|
+
def tooltip_options
|
|
7
|
+
{
|
|
8
|
+
text: tooltip_value(:text),
|
|
9
|
+
event: tooltip_value(:event)
|
|
10
|
+
}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def validate_tooltip!
|
|
16
|
+
return if tooltip.blank?
|
|
17
|
+
return if tooltip.is_a?(Hash) && tooltip_key?(:text) && tooltip_key?(:event)
|
|
18
|
+
|
|
19
|
+
raise ArgumentError, 'Tooltip must be a hash with :text and :event keys.'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def tooltip_key?(key)
|
|
23
|
+
tooltip.key?(key) || tooltip.key?(key.to_s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def tooltip_value(key)
|
|
27
|
+
tooltip[key] || tooltip[key.to_s]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
4
31
|
# Default Tramway button
|
|
5
32
|
#
|
|
6
33
|
class ButtonComponent < BaseComponent
|
|
@@ -17,12 +44,15 @@ module Tramway
|
|
|
17
44
|
option :size, default: -> { :medium }
|
|
18
45
|
option :method, optional: true, default: -> { :get }
|
|
19
46
|
option :tag, optional: true, default: -> {}
|
|
47
|
+
option :tooltip, optional: true, default: -> {}
|
|
20
48
|
option :options, optional: true, default: -> { {} }
|
|
21
49
|
option :form_options, optional: true, default: -> { {} }
|
|
22
50
|
|
|
23
51
|
include Tramway::ColorsMethods
|
|
52
|
+
include Tramway::ButtonComponentTooltip
|
|
24
53
|
|
|
25
54
|
def before_render
|
|
55
|
+
validate_tooltip!
|
|
26
56
|
return if tag.present?
|
|
27
57
|
|
|
28
58
|
@tag = if tag_button?
|
|
@@ -67,7 +97,7 @@ module Tramway
|
|
|
67
97
|
|
|
68
98
|
case normalized_type
|
|
69
99
|
when :default, :life, :secondary
|
|
70
|
-
['hover:bg-zinc-
|
|
100
|
+
['hover:bg-zinc-200', 'bg-zinc-50', 'text-zinc-950']
|
|
71
101
|
when :inverse
|
|
72
102
|
['hover:bg-zinc-800', 'bg-zinc-950', 'text-zinc-50', 'border', 'border-zinc-800']
|
|
73
103
|
else
|