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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d6347854d4ed5eb627a0adb496c8f6ffd7d5f5d4948c510586af26f42bc7a3f
4
- data.tar.gz: 52d08842b5fd4a6cb025d23e5faf8de44b699e4ed471b8c24049955bfab1a93f
3
+ metadata.gz: 000e5072249d738796fc44b63dccf1f228bd028eec1b63526450e2c69f5ddea5
4
+ data.tar.gz: 97b97098ee9f835a6372411cd7dee96ef5e16011f3262479c43e72077a07ec87
5
5
  SHA512:
6
- metadata.gz: b92182b455ad051e1e08e7020674ff3079acb09afba3e6845ad53b284cdf3fbe6844855b1b948a9066a990e3d669833a70d1cac04b62c15bf6fca22b27b0cbb9
7
- data.tar.gz: 0b9a66a185ab03f7f345ff966b16c6b65558e6487356c4b4a7c580decf1c579f7a1e3d8ac9ca2e05fe8859da40abc284c06ea5a43aff7f758b332d618514ef1a
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
- * `tramway_badge` renders a Tailwind-styled badge with the provided `text`. Pass a semantic `type` (for example, `:success` or
1134
- `:danger`) to use the built-in color mappings, or supply a custom Tailwind color family with `color:`. When you opt into a
1135
- custom color, ensure the corresponding background utilities are available in your Tailwind safelist.
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-select', to: 'tramway/tramway-select_controller.js'
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-select" // importing TramwaySelect controller class
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
- theme_classes(
15
- classic: [
16
- 'flex', 'px-3', 'py-1', 'text-sm', 'font-semibold', 'rounded-full', 'w-fit', 'h-fit',
17
- "bg-#{resolved_color}-700", "text-#{resolved_color}-100", 'shadow-md'
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
- - if text.present?
2
- - if stop_cell_propagation?
3
- = helpers.button_to text, path, method:, form: form_options, class: classes, **render_options
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
- - if stop_cell_propagation?
15
- = helpers.button_to path, method:, class: classes, form: form_options, **render_options do
16
- = content
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
- - case @tag
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-250', 'bg-zinc-50', 'text-zinc-950']
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
@@ -103,7 +103,7 @@ module Tramway
103
103
  Tramway::ButtonComponent.new(
104
104
  text: action,
105
105
  size: form_size,
106
- type: :inverse,
106
+ type: :default,
107
107
  options: sanitized_options.merge(name: :commit, type: :submit)
108
108
  ),
109
109
  &