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.
@@ -0,0 +1,12 @@
1
+ - if normalized_event == :onclick
2
+ %div{ class: wrapper_classes, **wrapper_options }
3
+ %div{ class: trigger_classes, data: trigger_data }
4
+ = content
5
+ %span{ class: tooltip_classes, role: 'tooltip', data: tooltip_data }
6
+ = text
7
+ - else
8
+ %div{ class: wrapper_classes, **wrapper_options }
9
+ %div{ class: trigger_classes }
10
+ = content
11
+ %span{ class: tooltip_classes, role: 'tooltip', data: tooltip_data }
12
+ = text
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ # Default Tramway tooltip
5
+ #
6
+ class TooltipComponent < Tramway::BaseComponent
7
+ EVENTS = %i[hover onclick].freeze
8
+
9
+ option :text
10
+ option :event, default: -> { :hover }
11
+ option :options, optional: true, default: -> { {} }
12
+
13
+ def wrapper_classes
14
+ (base_wrapper_classes + options[:class].to_s.split).compact.join(' ')
15
+ end
16
+
17
+ def trigger_classes
18
+ {
19
+ hover: %w[peer inline-flex w-fit],
20
+ onclick: %w[inline-flex w-fit cursor-pointer]
21
+ }[normalized_event].join(' ')
22
+ end
23
+
24
+ def wrapper_data
25
+ return options[:data] || {} unless normalized_event == :onclick
26
+
27
+ (options[:data] || {}).merge(
28
+ controller: merged_controller,
29
+ action: merged_action
30
+ )
31
+ end
32
+
33
+ def wrapper_options
34
+ options.except(:class, :data).merge(data: wrapper_data)
35
+ end
36
+
37
+ def trigger_data
38
+ return {} unless normalized_event == :onclick
39
+
40
+ { action: 'click->tramway-tooltip#toggle' }
41
+ end
42
+
43
+ def tooltip_data
44
+ return {} unless normalized_event == :onclick
45
+
46
+ { 'tramway-tooltip-target': 'panel' }
47
+ end
48
+
49
+ def tooltip_classes
50
+ (base_tooltip_classes + event_tooltip_classes).join(' ')
51
+ end
52
+
53
+ def normalized_event
54
+ event.to_sym
55
+ end
56
+
57
+ private
58
+
59
+ def before_render
60
+ return if EVENTS.include?(normalized_event)
61
+
62
+ raise ArgumentError, "Invalid event: #{event}. Valid events are :hover, :onclick."
63
+ end
64
+
65
+ def base_wrapper_classes
66
+ %w[relative inline-flex w-fit]
67
+ end
68
+
69
+ def base_tooltip_classes
70
+ %w[
71
+ absolute bottom-full left-1/2 z-50 mb-2 w-max min-w-40 max-w-sm -translate-x-1/2 rounded-md border
72
+ border-zinc-800 bg-zinc-950 px-2.5 py-1.5 text-xs font-medium leading-5 text-zinc-50 shadow-lg
73
+ ]
74
+ end
75
+
76
+ def event_tooltip_classes
77
+ if normalized_event == :hover
78
+ return %w[pointer-events-none invisible opacity-0 transition-opacity peer-hover:visible
79
+ peer-hover:opacity-100]
80
+ end
81
+
82
+ %w[hidden]
83
+ end
84
+
85
+ def merged_controller
86
+ [options.dig(:data, :controller), 'tramway-tooltip'].compact_blank.join(' ')
87
+ end
88
+
89
+ def merged_action
90
+ [options.dig(:data, :action), 'click@window->tramway-tooltip#closeOnClickOutside'].compact_blank.join(' ')
91
+ end
92
+ end
93
+ end
@@ -21,4 +21,4 @@
21
21
  = f.submit t('tramway.actions.save')
22
22
  = tramway_button text: t('tramway.actions.cancel'),
23
23
  path: public_send(@entity.index_helper_method),
24
- type: :fear
24
+ type: :inverse
@@ -236,7 +236,7 @@ module.exports = {
236
236
  'disabled:opacity-50',
237
237
  'h-10',
238
238
 
239
- 'hover:bg-zinc-250',
239
+ 'hover:bg-zinc-200',
240
240
  'bg-zinc-50',
241
241
  'text-zinc-950',
242
242
  'bg-zinc-950',
@@ -364,6 +364,29 @@ module.exports = {
364
364
  'placeholder-gray-500',
365
365
 
366
366
  // === Button and badge helpers ===
367
+ 'peer',
368
+ 'peer-hover:visible',
369
+ 'peer-hover:opacity-100',
370
+ 'group',
371
+ 'group-hover:visible',
372
+ 'group-hover:opacity-100',
373
+ 'open:block',
374
+ 'bottom-full',
375
+ 'left-1/2',
376
+ '-translate-x-1/2',
377
+ 'z-50',
378
+ 'mb-2',
379
+ 'w-max',
380
+ 'min-w-40',
381
+ 'max-w-sm',
382
+ 'invisible',
383
+ 'opacity-0',
384
+ 'transition-opacity',
385
+ 'pointer-events-none',
386
+ 'leading-5',
387
+ 'list-none',
388
+ 'cursor-pointer',
389
+ 'py-1.5',
367
390
  'rounded',
368
391
  'rounded-full',
369
392
  'rounded-xl',
@@ -374,10 +397,27 @@ module.exports = {
374
397
  'h-fit',
375
398
  'gap-1',
376
399
  'h-10',
400
+ 'px-2.5',
401
+ 'py-0.5',
377
402
  'shadow-md',
378
403
  'shadow-inner',
379
404
  'shadow-lg',
405
+ 'shadow',
380
406
  'h-12',
407
+ 'border-transparent',
408
+ 'focus:ring-2',
409
+ 'focus:ring-zinc-400',
410
+ 'focus:ring-offset-2',
411
+ 'focus:ring-offset-zinc-950',
412
+ 'hover:bg-zinc-800/80',
413
+ 'border-green-800',
414
+ 'border-orange-800',
415
+ 'border-red-800',
416
+ 'border-violet-800',
417
+ 'border-indigo-800',
418
+ 'border-yellow-800',
419
+ 'border-blue-800',
420
+ 'border-gray-800',
381
421
 
382
422
  // === Shadcn UI button styles ===
383
423
  'bg-green-600',
@@ -55,16 +55,8 @@ module Tramway
55
55
  @controllers_index_path ||= File.join(destination_root, 'app/javascript/controllers/index.js')
56
56
  end
57
57
 
58
- def importmap_tramway_select_pin
59
- 'pin "@tramway/tramway-select", to: "tramway/tramway-select_controller.js"'
60
- end
61
-
62
- def importmap_table_row_preview_pin
63
- 'pin "@tramway/table-row-preview", to: "tramway/table_row_preview_controller.js"'
64
- end
65
-
66
- def importmap_ui_checkbox_pin
67
- 'pin "@tramway/checkbox", to: "tramway/ui_checkbox_controller.js"'
58
+ def importmap_tramway_pin
59
+ 'pin "@tramway/tramway", to: "tramway/tramway.js"'
68
60
  end
69
61
 
70
62
  def importmap_tailwind_requirements
@@ -80,17 +72,13 @@ module Tramway
80
72
  def importmap_tramway_pins
81
73
  [
82
74
  importmap_tailwind_requirements,
83
- importmap_tramway_select_pin,
84
- importmap_table_row_preview_pin,
85
- importmap_ui_checkbox_pin
75
+ importmap_tramway_pin
86
76
  ]
87
77
  end
88
78
 
89
79
  def stimulus_controller_imports
90
80
  [
91
- 'import { TramwaySelect } from "@tramway/tramway-select"',
92
- 'import { TableRowPreview } from "@tramway/table-row-preview"',
93
- 'import { UiCheckbox } from "@tramway/checkbox"'
81
+ 'import { TramwaySelect, TableRowPreview, UiCheckbox, Tooltip } from "@tramway/tramway"'
94
82
  ]
95
83
  end
96
84
 
@@ -98,7 +86,8 @@ module Tramway
98
86
  [
99
87
  "application.register('tramway-select', TramwaySelect)",
100
88
  "application.register('table-row-preview', TableRowPreview)",
101
- "application.register('ui--checkbox', UiCheckbox)"
89
+ "application.register('ui--checkbox', UiCheckbox)",
90
+ "application.register('tramway-tooltip', Tooltip)"
102
91
  ]
103
92
  end
104
93
 
@@ -162,7 +151,7 @@ module Tramway
162
151
 
163
152
  # rubocop:disable Metrics/MethodLength
164
153
  def append_missing_imports(content)
165
- content = normalize_checkbox_import(content)
154
+ content = remove_legacy_stimulus_imports(content)
166
155
  missing_imports = stimulus_controller_imports.reject { |line| content.include?(line) }
167
156
  return content if missing_imports.empty?
168
157
 
@@ -198,21 +187,44 @@ module Tramway
198
187
  updated
199
188
  end
200
189
 
201
- def normalize_checkbox_import(content)
202
- content.gsub(
190
+ def remove_legacy_stimulus_imports(content)
191
+ legacy_imports = [
192
+ 'import { TramwaySelect } from "@tramway/tramway-select"',
193
+ 'import { TableRowPreview } from "@tramway/table-row-preview"',
194
+ 'import { UiCheckbox } from "@tramway/checkbox"',
203
195
  'import { UiCheckbox } from "@tramway/ui-checkbox"',
204
- 'import { UiCheckbox } from "@tramway/checkbox"'
205
- )
196
+ 'import { Tooltip } from "@tramway/tooltip"'
197
+ ]
198
+
199
+ content.each_line.reject { |line| legacy_imports.include?(line.strip) }.join
206
200
  end
207
201
 
208
- def normalize_checkbox_pin(content)
209
- content.gsub(
202
+ def remove_legacy_importmap_pins(content)
203
+ content.each_line.reject { |line| legacy_importmap_pins.include?(line.strip) }.join
204
+ end
205
+
206
+ def legacy_importmap_pins
207
+ double_quoted_legacy_importmap_pins + single_quoted_legacy_importmap_pins
208
+ end
209
+
210
+ def double_quoted_legacy_importmap_pins
211
+ [
212
+ 'pin "@tramway/tramway-select", to: "tramway/tramway-select_controller.js"',
213
+ 'pin "@tramway/table-row-preview", to: "tramway/table_row_preview_controller.js"',
214
+ 'pin "@tramway/checkbox", to: "tramway/ui_checkbox_controller.js"',
210
215
  'pin "@tramway/ui-checkbox", to: "tramway/ui_checkbox_controller.js"',
211
- 'pin "@tramway/checkbox", to: "tramway/ui_checkbox_controller.js"'
212
- ).gsub(
216
+ 'pin "@tramway/tooltip", to: "tramway/tooltip_controller.js"'
217
+ ]
218
+ end
219
+
220
+ def single_quoted_legacy_importmap_pins
221
+ [
222
+ "pin '@tramway/tramway-select', to: 'tramway/tramway-select_controller.js'",
223
+ "pin '@tramway/table-row-preview', to: 'tramway/table_row_preview_controller.js'",
224
+ "pin '@tramway/checkbox', to: 'tramway/ui_checkbox_controller.js'",
213
225
  "pin '@tramway/ui-checkbox', to: 'tramway/ui_checkbox_controller.js'",
214
- "pin '@tramway/checkbox', to: 'tramway/ui_checkbox_controller.js'"
215
- )
226
+ "pin '@tramway/tooltip', to: 'tramway/tooltip_controller.js'"
227
+ ]
216
228
  end
217
229
 
218
230
  def with_agents_update_fallback
@@ -290,7 +302,7 @@ module Tramway
290
302
  def ensure_importmap_pin
291
303
  return unless File.exist?(importmap_path)
292
304
 
293
- content = normalize_checkbox_pin(File.read(importmap_path))
305
+ content = remove_legacy_importmap_pins(File.read(importmap_path))
294
306
  missing_pins = importmap_tramway_pins.reject { |pin| content.include?(pin) }
295
307
  File.write(importmap_path, content) if content != File.read(importmap_path)
296
308
  return if missing_pins.empty?
@@ -17,7 +17,9 @@ module Tramway
17
17
  end
18
18
 
19
19
  initializer 'tramway.assets.precompile' do |app|
20
- app.config.assets.precompile += %w[tramway/tramway-select_controller.js tramway/ui_checkbox_controller.js]
20
+ app.config.assets.precompile += %w[
21
+ tramway/tramway.js
22
+ ]
21
23
  end
22
24
 
23
25
  private
@@ -46,7 +46,7 @@ module Tramway
46
46
  def tramway_button(path: nil, text: nil, method: :get, form_options: {}, **options, &)
47
47
  component 'tramway/button', text:, path:, method:, form_options:, color: options.delete(:color),
48
48
  type: options.delete(:type), size: options.delete(:size),
49
- tag: options.delete(:tag), options:, &
49
+ tag: options.delete(:tag), tooltip: options.delete(:tooltip), options:, &
50
50
  end
51
51
 
52
52
  def tramway_back_button
@@ -72,6 +72,10 @@ module Tramway
72
72
  color:
73
73
  end
74
74
 
75
+ def tramway_tooltip(text:, event: :hover, **options, &)
76
+ component 'tramway/tooltip', text:, event:, options:, &
77
+ end
78
+
75
79
  def tramway_title(text: nil, **options, &)
76
80
  component 'tramway/title', text:, options:, &
77
81
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tramway
4
- VERSION = '3.1'
4
+ VERSION = '3.1.1'
5
5
  end
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.1'
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kalashnikovisme
@@ -138,9 +138,7 @@ files:
138
138
  - MIT-LICENSE
139
139
  - README.md
140
140
  - Rakefile
141
- - app/assets/javascripts/tramway/table_row_preview_controller.js
142
- - app/assets/javascripts/tramway/tramway-select_controller.js
143
- - app/assets/javascripts/tramway/ui_checkbox_controller.js
141
+ - app/assets/javascripts/tramway/tramway.js
144
142
  - app/components/tailwind_component.html.haml
145
143
  - app/components/tailwind_component.rb
146
144
  - app/components/tramway/actions_buttons_container_component.html.haml
@@ -242,6 +240,8 @@ files:
242
240
  - app/components/tramway/table_component.rb
243
241
  - app/components/tramway/title_component.html.haml
244
242
  - app/components/tramway/title_component.rb
243
+ - app/components/tramway/tooltip_component.html.haml
244
+ - app/components/tramway/tooltip_component.rb
245
245
  - app/controllers/tramway/entities_controller.rb
246
246
  - app/helpers/tramway/application_helper.rb
247
247
  - app/views/kaminari/_first_page.html.haml
@@ -323,7 +323,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
323
323
  - !ruby/object:Gem::Version
324
324
  version: '0'
325
325
  requirements: []
326
- rubygems_version: 4.0.6
326
+ rubygems_version: 4.0.10
327
327
  specification_version: 4
328
328
  summary: Tramway Rails Engine
329
329
  test_files: []
@@ -1,175 +0,0 @@
1
- import { Controller } from "@hotwired/stimulus";
2
-
3
- export default class TableRowPreview extends Controller {
4
- connect() {
5
- this.items = JSON.parse(this.element.dataset.items || '{}');
6
- this.attachSwipeGesture();
7
- }
8
-
9
- disconnect() {
10
- this.detachSwipeGesture();
11
- }
12
-
13
- toggle() {
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
- }
25
-
26
- const existingTable = rollUp.querySelector(".div-table");
27
- if (existingTable) {
28
- existingTable.remove();
29
- }
30
-
31
- const existingTitle = rollUp.querySelector("h3");
32
- if (existingTitle) {
33
- existingTitle.remove();
34
- }
35
-
36
- const titleText = document.createElement("h3");
37
-
38
- titleText.classList.add("text-xl");
39
- titleText.classList.add("text-white");
40
- titleText.classList.add("py-4");
41
- titleText.classList.add("px-4");
42
- titleText.textContent = Object.values(this.items)[0];
43
-
44
- const table = this.createTable(this.items);
45
-
46
- rollUp.insertAdjacentElement('afterbegin', table);
47
- rollUp.insertAdjacentElement('afterbegin', titleText);
48
-
49
- rollUp.classList.remove("hidden");
50
- }
51
-
52
- close() {
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 = "";
136
- }
137
-
138
- createTable(items) {
139
- const table = document.createElement("div");
140
- table.classList.add("div-table");
141
- table.classList.add("text-white");
142
- table.classList.add("px-2");
143
-
144
- Object.entries(items).forEach(([key, value]) => {
145
- const rows = this.createTableRow(key, value);
146
-
147
- rows.forEach((row) => table.appendChild(row));
148
- });
149
-
150
- return table;
151
- }
152
-
153
- createTableRow(key, value) {
154
- const keyRow = document.createElement("div");
155
- keyRow.classList.add("div-table-row");
156
- keyRow.classList.add("bg-gray-700");
157
- keyRow.classList.add("text-white");
158
- keyRow.classList.add("px-2");
159
- keyRow.classList.add("py-1");
160
- keyRow.classList.add("text-xs");
161
- keyRow.classList.add("font-semibold");
162
- keyRow.textContent = key;
163
-
164
- const valueRow = document.createElement("div");
165
- valueRow.classList.add("div-table-row");
166
- valueRow.classList.add("bg-gray-800");
167
- valueRow.classList.add("px-2");
168
- valueRow.classList.add("py-2");
169
- valueRow.textContent = value;
170
-
171
- return [keyRow, valueRow];
172
- }
173
- }
174
-
175
- export { TableRowPreview };