tramway 3.1.2.8 → 4.0

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -16
  3. data/app/assets/builds/tailwind.css +2 -0
  4. data/app/assets/javascripts/tramway/tramway.js +174 -1
  5. data/app/assets/stylesheets/font-awesome.css +2337 -0
  6. data/app/assets/tailwind/application.css +29 -0
  7. data/app/components/tramway/chat_component.html.haml +1 -1
  8. data/app/components/tramway/containers/main_component.rb +1 -3
  9. data/app/components/tramway/containers/narrow_component.rb +5 -4
  10. data/app/components/tramway/form/builder.rb +1 -2
  11. data/app/components/tramway/grid/card_component.html.haml +2 -0
  12. data/app/components/tramway/grid/card_component.rb +37 -0
  13. data/app/components/tramway/grid_component.html.haml +18 -0
  14. data/app/components/tramway/grid_component.rb +76 -0
  15. data/app/components/tramway/native_text_component.rb +1 -2
  16. data/app/components/tramway/nav/item_component.rb +4 -2
  17. data/app/components/tramway/navbar_component.html.haml +104 -110
  18. data/app/components/tramway/navbar_component.rb +84 -2
  19. data/app/components/tramway/table/cell_component.rb +9 -1
  20. data/app/components/tramway/table/header_component.rb +23 -2
  21. data/app/components/tramway/table/row_component.rb +27 -10
  22. data/app/components/tramway/table_component.rb +28 -1
  23. data/app/controllers/tramway/entities_controller.rb +6 -6
  24. data/app/views/tramway/layouts/application.html.haml +8 -6
  25. data/config/routes.rb +1 -2
  26. data/config/tailwind.config.js +57 -0
  27. data/lib/generators/tramway/install/font_awesome_helpers.rb +39 -0
  28. data/lib/generators/tramway/install/install_generator.rb +131 -65
  29. data/lib/generators/tramway/install/layout_helpers.rb +95 -0
  30. data/lib/tramway/engine.rb +12 -0
  31. data/lib/tramway/forms/validations.rb +1 -2
  32. data/lib/tramway/helpers/navbar_helper.rb +3 -1
  33. data/lib/tramway/helpers/table_helper.rb +27 -0
  34. data/lib/tramway/helpers/views_helper.rb +26 -2
  35. data/lib/tramway/version.rb +1 -1
  36. metadata +28 -4
@@ -6,9 +6,20 @@ module Tramway
6
6
  class RowComponent < Tramway::BaseComponent
7
7
  include ContentCells
8
8
 
9
- DESKTOP_ROW_CLASSES = %w[
10
- div-table-row grid grid-cols-1 gap-4 border-b border-zinc-800 bg-zinc-950 last:border-b-0
11
- ].freeze
9
+ SIZE_CLASSES = {
10
+ small: {
11
+ row: 'div-table-row grid grid-cols-1 gap-2 border-b border-zinc-800 bg-transparent last:border-b-0',
12
+ cell: 'div-table-cell bg-transparent px-4 py-2 text-sm font-medium text-zinc-100 sm:text-sm'
13
+ },
14
+ medium: {
15
+ row: 'div-table-row grid grid-cols-1 gap-4 border-b border-zinc-800 bg-transparent last:border-b-0',
16
+ cell: 'div-table-cell bg-transparent px-6 py-4 text-xs font-medium text-zinc-100 sm:text-base'
17
+ },
18
+ large: {
19
+ row: 'div-table-row grid grid-cols-1 gap-6 border-b border-zinc-800 bg-transparent last:border-b-0',
20
+ cell: 'div-table-cell bg-transparent px-6 py-6 text-lg font-medium text-zinc-100'
21
+ }
22
+ }.freeze
12
23
 
13
24
  option :cells, optional: true, default: -> { [] }
14
25
  option :href, optional: true
@@ -32,7 +43,7 @@ module Tramway
32
43
  end
33
44
 
34
45
  def desktop_row_classes(cells_count)
35
- (DESKTOP_ROW_CLASSES + ["md:grid-cols-#{cells_count}"]).join(' ')
46
+ "#{size_classes.fetch(:row)} md:grid-cols-#{cells_count}"
36
47
  end
37
48
 
38
49
  def link_row_classes
@@ -40,25 +51,31 @@ module Tramway
40
51
  end
41
52
 
42
53
  def cell_classes
43
- 'div-table-cell px-6 py-4 text-xs font-medium text-zinc-100 sm:text-base'
54
+ size_classes.fetch(:cell)
44
55
  end
45
56
 
46
57
  def around_render
47
58
  ensure_view_context_accessor
48
- previous_flag = view_context.tramway_inside_cell
49
- view_context.tramway_inside_cell = href.present?
59
+ context = tramway_table_context
60
+ previous_flag = context.tramway_inside_cell
61
+ context.tramway_inside_cell = href.present?
50
62
 
51
63
  yield
52
64
  ensure
53
- view_context.tramway_inside_cell = previous_flag
65
+ context.tramway_inside_cell = previous_flag if context.respond_to?(:tramway_inside_cell=)
54
66
  end
55
67
 
56
68
  private
57
69
 
58
70
  def ensure_view_context_accessor
59
- return if view_context.respond_to?(:tramway_inside_cell=)
71
+ context = tramway_table_context
72
+ return if context.respond_to?(:tramway_inside_cell=)
60
73
 
61
- view_context.singleton_class.attr_accessor :tramway_inside_cell
74
+ context.singleton_class.attr_accessor :tramway_inside_cell
75
+ end
76
+
77
+ def size_classes
78
+ SIZE_CLASSES.fetch(tramway_table_size) { SIZE_CLASSES[:medium] }
62
79
  end
63
80
  end
64
81
  end
@@ -3,10 +3,37 @@
3
3
  module Tramway
4
4
  # Table component for rendering a table
5
5
  class TableComponent < Tramway::BaseComponent
6
+ TABLE_SIZES = %i[small medium large].freeze
7
+
8
+ option :size, default: -> { :medium }
6
9
  option :options, optional: true, default: -> { {} }
7
10
 
8
11
  def table_classes
9
- 'div-table w-full text-left rtl:text-right text-zinc-100'
12
+ 'div-table w-full bg-zinc-950 text-left rtl:text-right text-zinc-100'
13
+ end
14
+
15
+ def around_render
16
+ ensure_view_context_accessor
17
+ context = tramway_table_context
18
+ previous_size = context.tramway_table_size
19
+ context.tramway_table_size = normalized_size
20
+
21
+ yield
22
+ ensure
23
+ context.tramway_table_size = previous_size if context.respond_to?(:tramway_table_size=)
24
+ end
25
+
26
+ private
27
+
28
+ def normalized_size
29
+ TABLE_SIZES.include?(size) ? size : :medium
30
+ end
31
+
32
+ def ensure_view_context_accessor
33
+ context = tramway_table_context
34
+ return if context.respond_to?(:tramway_table_size=)
35
+
36
+ context.singleton_class.attr_accessor :tramway_table_size
10
37
  end
11
38
  end
12
39
  end
@@ -28,7 +28,7 @@ module Tramway
28
28
 
29
29
  def show
30
30
  @record = tramway_decorate(
31
- model_class.find(params[:id]),
31
+ model_class.find(params.expect(:id)),
32
32
  namespace: entity.namespace
33
33
  ).with(view_context:)
34
34
 
@@ -40,14 +40,14 @@ module Tramway
40
40
  end
41
41
 
42
42
  def edit
43
- @record = tramway_form model_class.find(params[:id]), namespace: entity.namespace
43
+ @record = tramway_form model_class.find(params.expect(:id)), namespace: entity.namespace
44
44
  end
45
45
 
46
46
  # rubocop:disable Metrics/AbcSize
47
47
  def create
48
48
  @record = tramway_form model_class.new, namespace: entity.namespace
49
49
 
50
- if @record.submit params[model_class.model_name.param_key]
50
+ if @record.submit params.require(model_class.model_name.param_key).permit!
51
51
  redirect_to Tramway::Engine.routes.url_helpers.public_send(entity.show_helper_method, @record.id),
52
52
  notice: t('tramway.notices.created')
53
53
  else
@@ -56,9 +56,9 @@ module Tramway
56
56
  end
57
57
 
58
58
  def update
59
- @record = tramway_form model_class.find(params[:id]), namespace: entity.namespace
59
+ @record = tramway_form model_class.find(params.expect(:id)), namespace: entity.namespace
60
60
 
61
- if @record.submit params[model_class.model_name.param_key]
61
+ if @record.submit params.require(model_class.model_name.param_key).permit!
62
62
  redirect_to Tramway::Engine.routes.url_helpers.public_send(entity.show_helper_method, @record.id),
63
63
  notice: t('tramway.notices.updated')
64
64
  else
@@ -68,7 +68,7 @@ module Tramway
68
68
  # rubocop:enable Metrics/AbcSize
69
69
 
70
70
  def destroy
71
- @record = model_class.find(params[:id])
71
+ @record = model_class.find(params.expect(:id))
72
72
 
73
73
  @record.destroy
74
74
 
@@ -14,6 +14,7 @@
14
14
  %link{rel: "apple-touch-icon", href: "/icon.png"}
15
15
 
16
16
  = stylesheet_link_tag "tailwind", "data-turbo-track": "reload"
17
+ = stylesheet_link_tag "font-awesome", "data-turbo-track": "reload"
17
18
  = stylesheet_link_tag "trix", "data-turbo-track": "reload"
18
19
  = javascript_include_tag "trix", "data-turbo-track": "reload", defer: true
19
20
  = javascript_importmap_tags
@@ -21,10 +22,11 @@
21
22
  %body.bg-zinc-950.text-zinc-50
22
23
  = tramway_navbar title: 'Tramway'
23
24
 
24
- - if flash.any?
25
- = tramway_flash text: flash[:notice].presence || flash[:alert],
26
- type: flash[:notice].present? ? :will : :rage,
27
- id: 'flash-container'
25
+ = tramway_main_container id: 'tramway-main-container', class: 'md:pl-72 transition-[padding-left] duration-300 ease-in-out-strong motion-reduce:duration-0' do
26
+ - if flash.any?
27
+ = tramway_flash text: flash[:notice].presence || flash[:alert],
28
+ type: flash[:notice].present? ? :will : :rage,
29
+ id: 'flash-container'
28
30
 
29
- .container.mx-auto.p-4.flex.align-center.justify-center
30
- = yield
31
+ .container.mx-auto.p-4.flex.align-center.justify-center
32
+ = yield
data/config/routes.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'tramway/helpers/routes_helper'
4
4
 
5
- # rubocop:disable Metrics/BlockLength
5
+ # rubocop:disable-next Metrics/BlockLength
6
6
  Tramway::Engine.routes.draw do
7
7
  Tramway.config.entities.each do |entity|
8
8
  if entity.namespace.present?
@@ -55,7 +55,6 @@ Tramway::Engine.routes.draw do
55
55
  end
56
56
  end
57
57
  end
58
- # rubocop:enable Metrics/BlockLength
59
58
 
60
59
  Tramway::Engine.routes.routes.map(&:name).compact.each do |route_name|
61
60
  %w[path url].each do |suffix|
@@ -1,5 +1,8 @@
1
1
  const defaultTheme = require("tailwindcss/defaultTheme")
2
2
 
3
+ const buildUtilities = (prefix, start, end) =>
4
+ Array.from({ length: end - start + 1 }, (_, index) => `${prefix}${start + index}`)
5
+
3
6
  module.exports = {
4
7
  safelist: [
5
8
  // === Navbar ===
@@ -13,17 +16,52 @@ module.exports = {
13
16
  'md:translate-x-0',
14
17
  'border-white',
15
18
  'h-screen',
19
+ 'h-dvh',
16
20
  'w-screen',
21
+ 'w-72',
17
22
  'z-50',
18
23
  'inset-0',
24
+ 'top-0',
19
25
  'top-4',
20
26
  'right-6',
27
+ 'right-0',
21
28
  'text-4xl',
22
29
  'transition-transform',
23
30
  '-translate-x-full',
24
31
  'duration-300',
25
32
  'space-y-3',
26
33
  'md:justify-between',
34
+ 'hidden',
35
+ 'md:fixed',
36
+ 'md:left-0',
37
+ 'md:top-0',
38
+ 'md:z-40',
39
+ 'md:h-dvh',
40
+ 'md:w-72',
41
+ 'md:w-24',
42
+ 'md:flex-col',
43
+ 'md:hidden',
44
+ 'md:border-r',
45
+ 'md:border-zinc-800',
46
+ 'md:bg-zinc-950',
47
+ 'md:px-4',
48
+ 'md:py-3',
49
+ 'md:shadow-sm',
50
+ 'md:backdrop-blur',
51
+ 'md:overflow-hidden',
52
+ 'md:pl-72',
53
+ 'md:pl-24',
54
+ 'transition-all',
55
+ 'ease-in-out',
56
+ 'md:transition-[width]',
57
+ 'transition-[padding-left]',
58
+ 'ease-in-out-strong',
59
+ 'md:ease-in-out-strong',
60
+ 'ease-out-strong',
61
+ 'md:motion-reduce:duration-0',
62
+ 'motion-reduce:duration-0',
63
+ 'duration-200',
64
+ 'rotate-180',
27
65
  'pt-16',
28
66
  'min-h-8',
29
67
  'bg-zinc-950',
@@ -55,6 +93,7 @@ module.exports = {
55
93
  'shadow-lg',
56
94
  'shadow-sm',
57
95
  'w-fit',
96
+ 'text-xs',
58
97
  'text-base',
59
98
  'text-sm',
60
99
  'text-lg',
@@ -64,11 +103,23 @@ module.exports = {
64
103
  'border-r',
65
104
  'border-t',
66
105
  'border',
106
+ 'border-dotted',
107
+ 'border-zinc-700/50',
108
+ 'border-zinc-100/60',
109
+ 'box-border',
67
110
  'gap-4',
111
+ 'gap-6',
68
112
  'gap-1',
69
113
  'gap-2',
70
114
  'grid-cols-1',
71
115
  'grid',
116
+ 'overflow-auto',
117
+ 'inset-x-0',
118
+ 'inset-y-0',
119
+ ...buildUtilities('md:grid-cols-', 1, 12),
120
+ ...buildUtilities('md:grid-rows-', 1, 12),
121
+ ...buildUtilities('md:col-span-', 1, 12),
122
+ ...buildUtilities('md:row-span-', 1, 12),
72
123
  'px-4',
73
124
  'px-3',
74
125
  'py-3',
@@ -79,6 +130,7 @@ module.exports = {
79
130
  'inset-0',
80
131
  'top-4',
81
132
  'right-4',
133
+ 'z-10',
82
134
  'flex-col',
83
135
  'items-center',
84
136
  'justify-between',
@@ -173,6 +225,7 @@ module.exports = {
173
225
  'sm:px-3',
174
226
  'sm:py-2',
175
227
  'sm:text-sm',
228
+ 'sm:text-base',
176
229
  'md:gap-1',
177
230
  'md:space-y-4',
178
231
  'space-y-2',
@@ -651,6 +704,10 @@ module.exports = {
651
704
  fontFamily: {
652
705
  sans: ["Inter var", ...defaultTheme.fontFamily.sans],
653
706
  },
707
+ transitionTimingFunction: {
708
+ "out-strong": "cubic-bezier(0.23, 1, 0.32, 1)",
709
+ "in-out-strong": "cubic-bezier(0.77, 0, 0.175, 1)",
710
+ },
654
711
  keyframes: {
655
712
  "accordion-down": {
656
713
  from: { height: 0 },
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tramway
4
+ module Generators
5
+ # nodoc
6
+ module InstallGeneratorFontAwesomeHelpers
7
+ private
8
+
9
+ def font_awesome_already_present?(content)
10
+ content.include?('stylesheet_link_tag "font-awesome"') ||
11
+ content.include?("stylesheet_link_tag 'font-awesome'")
12
+ end
13
+
14
+ def ensure_font_awesome_in_haml_layout
15
+ content = File.read(application_layout_haml_path)
16
+ return if font_awesome_already_present?(content)
17
+ return unless content.match?(/^\s+= stylesheet_link_tag "tailwind", "data-turbo-track": "reload"/)
18
+
19
+ updated = content.sub(
20
+ /^(\s+= stylesheet_link_tag "tailwind", "data-turbo-track": "reload"\n)/,
21
+ "\\1 = stylesheet_link_tag \"font-awesome\", \"data-turbo-track\": \"reload\"\n"
22
+ )
23
+ File.write(application_layout_haml_path, updated)
24
+ end
25
+
26
+ def ensure_font_awesome_in_erb_layout
27
+ content = File.read(application_layout_erb_path)
28
+ return if font_awesome_already_present?(content)
29
+ return unless content.include?('</head>')
30
+
31
+ updated = content.sub(
32
+ /(\s+<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>\n)/,
33
+ "\\1 <%= stylesheet_link_tag \"font-awesome\", \"data-turbo-track\": \"reload\" %>\n"
34
+ )
35
+ File.write(application_layout_erb_path, updated)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -2,12 +2,47 @@
2
2
 
3
3
  require 'rails/generators'
4
4
  require 'fileutils'
5
+ require_relative 'font_awesome_helpers'
6
+ require_relative 'layout_helpers'
5
7
 
6
8
  module Tramway
7
9
  module Generators
8
10
  # nodoc
9
- # rubocop:disable Metrics/ModuleLength
11
+ # rubocop:disable-next Metrics/ModuleLength
10
12
  module InstallGeneratorHelpers
13
+ TAILWIND_SCROLLBAR_UTILITY = <<~CSS.chomp
14
+ @layer utilities {
15
+ .tramway-scrollbar {
16
+ scrollbar-width: thin;
17
+ scrollbar-color: rgba(82, 82, 91, 0.9) rgba(9, 9, 11, 0.92);
18
+ }
19
+
20
+ .tramway-scrollbar::-webkit-scrollbar {
21
+ width: 0.5rem;
22
+ }
23
+
24
+ .tramway-scrollbar::-webkit-scrollbar-track {
25
+ background: rgba(9, 9, 11, 0.92);
26
+ }
27
+
28
+ .tramway-scrollbar::-webkit-scrollbar-thumb {
29
+ border-radius: 9999px;
30
+ background: rgba(82, 82, 91, 0.9);
31
+ }
32
+
33
+ .tramway-scrollbar::-webkit-scrollbar-thumb:hover {
34
+ background: rgba(113, 113, 122, 0.95);
35
+ }
36
+
37
+ .tramway-scrollbar::-webkit-scrollbar-corner {
38
+ background: transparent;
39
+ }
40
+ }
41
+ CSS
42
+
43
+ TAILWIND_APPLICATION_STYLESHEET_CONTENT =
44
+ "@import \"tailwindcss\";\n\n#{TAILWIND_SCROLLBAR_UTILITY}\n".freeze
45
+
11
46
  private
12
47
 
13
48
  def gem_dependencies
@@ -78,12 +113,13 @@ module Tramway
78
113
 
79
114
  def stimulus_controller_imports
80
115
  [
81
- 'import { TramwaySelect, TableRowPreview, UiCheckbox, Tooltip } from "@tramway/tramway"'
116
+ 'import { Navbar, TramwaySelect, TableRowPreview, UiCheckbox, Tooltip } from "@tramway/tramway"'
82
117
  ]
83
118
  end
84
119
 
85
120
  def stimulus_controller_registrations
86
121
  [
122
+ "application.register('tramway-navbar', Navbar)",
87
123
  "application.register('tramway-select', TramwaySelect)",
88
124
  "application.register('table-row-preview', TableRowPreview)",
89
125
  "application.register('ui--checkbox', UiCheckbox)",
@@ -95,28 +131,6 @@ module Tramway
95
131
  @agents_file_path ||= File.join(destination_root, 'AGENTS.md')
96
132
  end
97
133
 
98
- def application_layout_haml_path
99
- @application_layout_haml_path ||= File.join(destination_root, 'app/views/layouts/application.html.haml')
100
- end
101
-
102
- def application_layout_erb_path
103
- @application_layout_erb_path ||= File.join(destination_root, 'app/views/layouts/application.html.erb')
104
- end
105
-
106
- def trix_haml_tags
107
- " = stylesheet_link_tag \"trix\", \"data-turbo-track\": \"reload\"\n " \
108
- "= javascript_include_tag \"trix\", \"data-turbo-track\": \"reload\", defer: true\n"
109
- end
110
-
111
- def trix_erb_tags
112
- " <%= stylesheet_link_tag \"trix\", \"data-turbo-track\": \"reload\" %>\n " \
113
- "<%= javascript_include_tag \"trix\", \"data-turbo-track\": \"reload\", defer: true %>\n"
114
- end
115
-
116
- def trix_already_present?(content)
117
- content.include?('stylesheet_link_tag "trix"') || content.include?("stylesheet_link_tag 'trix'")
118
- end
119
-
120
134
  def codex_agents_instruction
121
135
  'If you are Codex, use the tramway-skill skill for Tramway/Rails work in this project. ' \
122
136
  'If tramway-skill is not installed, install it from https://github.com/Purple-Magic/tramway-skill/.'
@@ -134,15 +148,16 @@ module Tramway
134
148
  end
135
149
 
136
150
  def safelist_section(content)
137
- match = content.match(/safelist\s*:\s*\[(.*?)\]\s*,?/m)
138
- match&.[](1)
151
+ open_index, close_index = safelist_bracket_range(content)
152
+ return nil unless open_index
153
+
154
+ content[(open_index + 1)...close_index]
139
155
  end
140
156
 
141
157
  def insert_entries(content, entries)
142
- match = content.match(/safelist\s*:\s*\[(.*?)\](\s*,?)/m)
143
- return content unless match
158
+ _open_index, closing_index = safelist_bracket_range(content)
159
+ return content unless closing_index
144
160
 
145
- closing_index = match.begin(0) + match[0].rindex(']')
146
161
  indentation = closing_indentation(content, closing_index)
147
162
  formatted_entries = entries.map { |entry| "#{indentation}'#{entry}',\n" }.join
148
163
 
@@ -154,6 +169,33 @@ module Tramway
154
169
  content.dup.insert(closing_index, insertion)
155
170
  end
156
171
 
172
+ # Finds the `[`/`]` pair that delimits the `safelist` array, ignoring brackets that
173
+ # appear inside quoted entries (e.g. `'md:transition-[width]'`) since a naive regex
174
+ # would otherwise stop at the first `]`, wherever it occurs.
175
+ def safelist_bracket_range(content)
176
+ start_index = content =~ /safelist\s*:\s*\[/
177
+ return [nil, nil] unless start_index
178
+
179
+ open_index = content.index('[', start_index)
180
+ close_index = matching_bracket_index(content, open_index)
181
+
182
+ [open_index, close_index]
183
+ end
184
+
185
+ def matching_bracket_index(content, open_index)
186
+ depth = 0
187
+
188
+ content[open_index..].to_enum(:scan, /'[^']*'|"[^"]*"|[\[\]]/).each do
189
+ token = Regexp.last_match[0]
190
+ next unless ['[', ']'].include?(token)
191
+
192
+ depth += token == '[' ? 1 : -1
193
+ return open_index + Regexp.last_match.end(0) - 1 if depth.zero?
194
+ end
195
+
196
+ nil
197
+ end
198
+
157
199
  def closing_indentation(content, index)
158
200
  line_start = content.rindex("\n", index - 1) || 0
159
201
  line = content[line_start..index]
@@ -171,30 +213,39 @@ module Tramway
171
213
  content.end_with?("\n") ? "\n" : "\n\n"
172
214
  end
173
215
 
174
- # rubocop:disable Metrics/MethodLength
175
216
  def append_missing_imports(content)
176
217
  content = remove_legacy_stimulus_imports(content)
218
+ content = normalize_tramway_stimulus_imports(content)
177
219
  missing_imports = stimulus_controller_imports.reject { |line| content.include?(line) }
178
220
  return content if missing_imports.empty?
179
221
 
180
222
  import_lines = content.each_line.with_index.filter_map do |line, index|
181
223
  index if line.lstrip.start_with?('import ')
182
224
  end
183
- insertion = "#{missing_imports.join("\n")}\n"
225
+ insert_missing_imports(content, missing_imports.join("\n"), import_lines)
226
+ end
227
+
228
+ def insert_missing_imports(content, imports, import_lines)
229
+ insertion = "#{imports}\n"
184
230
  updated = content.dup
185
231
 
186
- if import_lines.any?
187
- insertion_index = updated.lines[0..import_lines.max].join.length
188
- updated.insert(insertion_index, insertion)
189
- else
190
- updated.prepend(insertion)
191
- end
232
+ return updated.prepend(insertion) unless import_lines.any?
192
233
 
234
+ insertion_index = updated.lines[0..import_lines.max].join.length
235
+ updated.insert(insertion_index, insertion)
193
236
  updated
194
237
  end
195
- # rubocop:enable Metrics/MethodLength
238
+
239
+ def normalize_tramway_stimulus_imports(content)
240
+ tramway_import_lines = content.each_line.select { |line| tramway_stimulus_import_line?(line) }
241
+ return content unless tramway_import_lines.any?
242
+ return content if normalized_tramway_import_lines?(tramway_import_lines)
243
+
244
+ content.each_line.reject { |line| tramway_stimulus_import_line?(line) }.join
245
+ end
196
246
 
197
247
  def append_missing_registrations(content)
248
+ content = normalize_tramway_stimulus_registrations(content)
198
249
  missing_registrations = stimulus_controller_registrations.reject { |line| content.include?(line) }
199
250
  return content if missing_registrations.empty?
200
251
 
@@ -221,6 +272,36 @@ module Tramway
221
272
  content.each_line.reject { |line| legacy_imports.include?(line.strip) }.join
222
273
  end
223
274
 
275
+ def tramway_stimulus_import_line?(line)
276
+ line.strip.match?(%r{^import \{.*\} from ["']@tramway/tramway["']$})
277
+ end
278
+
279
+ def normalize_tramway_stimulus_registrations(content)
280
+ tramway_registration_lines = content.each_line.select { |line| tramway_stimulus_registration_line?(line) }
281
+ return content if tramway_registration_lines.sort == stimulus_controller_registrations.sort
282
+ return content unless tramway_registration_lines.any?
283
+
284
+ content.each_line.reject { |line| tramway_stimulus_registration_line?(line) }.join
285
+ end
286
+
287
+ def tramway_stimulus_registration_line?(line)
288
+ tramway_registration_prefixes.any? { |prefix| line.strip.start_with?(prefix) }
289
+ end
290
+
291
+ def normalized_tramway_import_lines?(tramway_import_lines)
292
+ tramway_import_lines.one? && tramway_import_lines.first.strip == stimulus_controller_imports.first
293
+ end
294
+
295
+ def tramway_registration_prefixes
296
+ [
297
+ "application.register('tramway-navbar'",
298
+ "application.register('tramway-select'",
299
+ "application.register('table-row-preview'",
300
+ "application.register('ui--checkbox'",
301
+ "application.register('tramway-tooltip'"
302
+ ]
303
+ end
304
+
224
305
  def remove_legacy_importmap_pins(content)
225
306
  content.each_line.reject { |line| legacy_importmap_pins.include?(line.strip) }.join
226
307
  end
@@ -255,12 +336,12 @@ module Tramway
255
336
  say_status(:warning, "Skipping AGENTS.md update: #{e.message}")
256
337
  end
257
338
  end
258
- # rubocop:enable Metrics/ModuleLength
259
339
 
260
- # Running `rails generate tramway:install` will invoke this generator
261
- #
340
+ # Installs Tramway dependencies and app wiring for a host Rails app.
262
341
  class InstallGenerator < Rails::Generators::Base
263
342
  include InstallGeneratorHelpers
343
+ include InstallGeneratorFontAwesomeHelpers
344
+ include InstallGeneratorLayoutHelpers
264
345
 
265
346
  desc 'Installs Tramway dependencies and Tailwind safelist configuration.'
266
347
 
@@ -310,15 +391,14 @@ module Tramway
310
391
  path = tailwind_application_stylesheet_path
311
392
  FileUtils.mkdir_p(File.dirname(path))
312
393
 
313
- return create_file(path, "#{tailwind_css_import_line}\n") unless File.exist?(path)
394
+ return create_file(path, TAILWIND_APPLICATION_STYLESHEET_CONTENT) unless File.exist?(path)
314
395
 
315
396
  content = File.read(path)
316
- return if content.include?(tailwind_css_import_line)
397
+ return if content.include?(tailwind_css_import_line) && content.include?(TAILWIND_SCROLLBAR_UTILITY)
317
398
 
318
- File.open(path, 'a') do |file|
319
- file.write("\n") unless content.empty? || content.end_with?("\n")
320
- file.write("#{tailwind_css_import_line}\n")
321
- end
399
+ updated = content.include?(tailwind_css_import_line) ? content : "#{content}\n#{tailwind_css_import_line}\n"
400
+ updated = "#{updated}\n#{TAILWIND_SCROLLBAR_UTILITY}\n" unless updated.include?(TAILWIND_SCROLLBAR_UTILITY)
401
+ File.write(path, updated)
322
402
  end
323
403
 
324
404
  def ensure_importmap_pin
@@ -348,30 +428,16 @@ module Tramway
348
428
 
349
429
  def ensure_trix_in_application_layout
350
430
  if File.exist?(application_layout_haml_path)
431
+ ensure_font_awesome_in_haml_layout
351
432
  ensure_trix_in_haml_layout
352
433
  elsif File.exist?(application_layout_erb_path)
434
+ ensure_font_awesome_in_erb_layout
353
435
  ensure_trix_in_erb_layout
354
436
  end
355
437
  end
356
438
 
357
- private
358
-
359
- def ensure_trix_in_haml_layout
360
- content = File.read(application_layout_haml_path)
361
- return if trix_already_present?(content)
362
- return unless content.match?(/^\s+%body/)
363
-
364
- updated = content.sub(/^(\s+%body)/, "#{trix_haml_tags}\\1")
365
- File.write(application_layout_haml_path, updated)
366
- end
367
-
368
- def ensure_trix_in_erb_layout
369
- content = File.read(application_layout_erb_path)
370
- return if trix_already_present?(content)
371
- return unless content.include?('</head>')
372
-
373
- updated = content.sub('</head>', "#{trix_erb_tags} </head>")
374
- File.write(application_layout_erb_path, updated)
439
+ def ensure_navbar_sidebar_offset_in_application_layout
440
+ ensure_navbar_sidebar_offset
375
441
  end
376
442
  end
377
443
  end