nitro_kit 0.9.0 → 2.0.0.alpha.2

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 (219) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/plugins/marketplace.json +20 -0
  3. data/CHANGELOG.md +172 -0
  4. data/LICENSE +28 -0
  5. data/README.md +22 -15
  6. data/STYLE_GUIDE.md +425 -0
  7. data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
  8. data/app/assets/stylesheets/nitro_kit.css +7239 -0
  9. data/app/components/nitro_kit/accordion.rb +125 -65
  10. data/app/components/nitro_kit/alert.rb +80 -43
  11. data/app/components/nitro_kit/app_navigation.rb +307 -0
  12. data/app/components/nitro_kit/app_shell.rb +227 -0
  13. data/app/components/nitro_kit/appearance_bootstrap.rb +146 -0
  14. data/app/components/nitro_kit/appearance_picker.rb +156 -0
  15. data/app/components/nitro_kit/auth_shell.rb +34 -0
  16. data/app/components/nitro_kit/avatar.rb +85 -29
  17. data/app/components/nitro_kit/avatar_stack.rb +155 -9
  18. data/app/components/nitro_kit/badge.rb +43 -81
  19. data/app/components/nitro_kit/button.rb +180 -103
  20. data/app/components/nitro_kit/button_group.rb +63 -11
  21. data/app/components/nitro_kit/button_to.rb +69 -0
  22. data/app/components/nitro_kit/card.rb +42 -45
  23. data/app/components/nitro_kit/checkbox.rb +115 -62
  24. data/app/components/nitro_kit/checkbox_group.rb +131 -22
  25. data/app/components/nitro_kit/choice.rb +55 -0
  26. data/app/components/nitro_kit/combobox.rb +292 -105
  27. data/app/components/nitro_kit/command_palette.rb +412 -0
  28. data/app/components/nitro_kit/component.rb +322 -46
  29. data/app/components/nitro_kit/container.rb +32 -0
  30. data/app/components/nitro_kit/control_group.rb +45 -0
  31. data/app/components/nitro_kit/danger_zone.rb +78 -0
  32. data/app/components/nitro_kit/data_section.rb +106 -0
  33. data/app/components/nitro_kit/details_table.rb +229 -0
  34. data/app/components/nitro_kit/dialog.rb +257 -83
  35. data/app/components/nitro_kit/dropdown.rb +335 -122
  36. data/app/components/nitro_kit/dropzone.rb +326 -0
  37. data/app/components/nitro_kit/empty_state.rb +99 -0
  38. data/app/components/nitro_kit/field.rb +461 -236
  39. data/app/components/nitro_kit/field_group.rb +15 -8
  40. data/app/components/nitro_kit/fieldset.rb +37 -39
  41. data/app/components/nitro_kit/flex.rb +49 -0
  42. data/app/components/nitro_kit/form_builder.rb +313 -72
  43. data/app/components/nitro_kit/form_section.rb +79 -0
  44. data/app/components/nitro_kit/grid.rb +34 -0
  45. data/app/components/nitro_kit/icon.rb +54 -25
  46. data/app/components/nitro_kit/input.rb +97 -10
  47. data/app/components/nitro_kit/label.rb +22 -6
  48. data/app/components/nitro_kit/layout_options.rb +7 -0
  49. data/app/components/nitro_kit/page_header.rb +76 -0
  50. data/app/components/nitro_kit/pagination.rb +355 -69
  51. data/app/components/nitro_kit/pagination_bar.rb +90 -0
  52. data/app/components/nitro_kit/progressive_image.rb +181 -0
  53. data/app/components/nitro_kit/radio_button.rb +77 -55
  54. data/app/components/nitro_kit/radio_button_group.rb +107 -36
  55. data/app/components/nitro_kit/responsive_value.rb +94 -0
  56. data/app/components/nitro_kit/rich_text_area.rb +31 -0
  57. data/app/components/nitro_kit/select.rb +129 -64
  58. data/app/components/nitro_kit/settings_layout.rb +152 -0
  59. data/app/components/nitro_kit/sheet.rb +202 -0
  60. data/app/components/nitro_kit/stat_grid.rb +75 -0
  61. data/app/components/nitro_kit/switch.rb +108 -51
  62. data/app/components/nitro_kit/table.rb +237 -45
  63. data/app/components/nitro_kit/tabs.rb +168 -63
  64. data/app/components/nitro_kit/textarea.rb +81 -10
  65. data/app/components/nitro_kit/toast.rb +211 -80
  66. data/app/components/nitro_kit/toolbar.rb +75 -0
  67. data/app/components/nitro_kit/tooltip.rb +235 -34
  68. data/app/components/nitro_kit/typeset.rb +28 -0
  69. data/app/javascript/controllers/nk/app_shell_controller.js +224 -0
  70. data/app/javascript/controllers/nk/appearance_controller.js +73 -0
  71. data/app/javascript/controllers/nk/avatar_controller.js +15 -0
  72. data/app/javascript/controllers/nk/button_controller.js +56 -0
  73. data/app/javascript/controllers/nk/checkable_controller.js +41 -0
  74. data/app/javascript/controllers/nk/combobox_controller.js +314 -82
  75. data/app/javascript/controllers/nk/command_palette_controller.js +249 -0
  76. data/app/javascript/controllers/nk/dialog_controller.js +14 -10
  77. data/app/javascript/controllers/nk/dropdown_controller.js +106 -58
  78. data/app/javascript/controllers/nk/dropzone/direct_upload.js +61 -0
  79. data/app/javascript/controllers/nk/dropzone/file_rules.js +77 -0
  80. data/app/javascript/controllers/nk/dropzone/form_submit_lock.js +30 -0
  81. data/app/javascript/controllers/nk/dropzone_controller.js +482 -0
  82. data/app/javascript/controllers/nk/overlay_position.js +37 -0
  83. data/app/javascript/controllers/nk/progressive_image_controller.js +116 -0
  84. data/app/javascript/controllers/nk/tabs_controller.js +135 -23
  85. data/app/javascript/controllers/nk/toast_controller.js +112 -45
  86. data/app/javascript/controllers/nk/tooltip_controller.js +12 -47
  87. data/config/importmap.rb +2 -0
  88. data/config/locales/en.yml +98 -0
  89. data/docs/agent_guide.md +139 -0
  90. data/docs/agent_native_spec.md +422 -0
  91. data/docs/component_contracts.md +263 -0
  92. data/docs/customization.md +595 -0
  93. data/docs/hotwire.md +94 -0
  94. data/docs/initialization_prompt.md +36 -0
  95. data/docs/migration_1_to_2.md +318 -0
  96. data/docs/new_app_strategy.md +22 -0
  97. data/docs/patterns/application_foundation.md +136 -0
  98. data/docs/patterns/crud_resource.md +144 -0
  99. data/docs/patterns/destructive_action.md +105 -0
  100. data/docs/patterns/flash_and_toast.md +57 -0
  101. data/docs/patterns/inline_edit.md +59 -0
  102. data/docs/patterns/queryable_collection.md +187 -0
  103. data/docs/patterns/resource_form.md +126 -0
  104. data/docs/rails_conventions.md +95 -0
  105. data/docs/rails_integration.md +514 -0
  106. data/lib/generators/nitro_kit/install_generator.rb +48 -0
  107. data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +67 -0
  108. data/lib/nitro_kit/engine.rb +20 -0
  109. data/lib/nitro_kit/installation.rb +671 -0
  110. data/lib/nitro_kit/migration_inventory.rb +345 -0
  111. data/lib/nitro_kit/upgrade_smoke_test.rb +403 -0
  112. data/lib/nitro_kit/version.rb +1 -1
  113. data/lib/nitro_kit.rb +5 -42
  114. data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +42 -0
  115. data/lib/tasks/nitro_kit_tasks.rake +79 -4
  116. data/plugins/nitro-kit/.codex-plugin/plugin.json +28 -0
  117. data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +67 -0
  118. data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +4 -0
  119. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +60 -0
  120. data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +4 -0
  121. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +62 -0
  122. data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +4 -0
  123. data/src/stylesheets/nitro_kit/components/accordion.css +119 -0
  124. data/src/stylesheets/nitro_kit/components/alert.css +82 -0
  125. data/src/stylesheets/nitro_kit/components/app_navigation.css +156 -0
  126. data/src/stylesheets/nitro_kit/components/app_shell.css +605 -0
  127. data/src/stylesheets/nitro_kit/components/appearance_picker.css +117 -0
  128. data/src/stylesheets/nitro_kit/components/auth_shell.css +8 -0
  129. data/src/stylesheets/nitro_kit/components/avatar.css +81 -0
  130. data/src/stylesheets/nitro_kit/components/avatar_stack.css +80 -0
  131. data/src/stylesheets/nitro_kit/components/badge.css +76 -0
  132. data/src/stylesheets/nitro_kit/components/button.css +283 -0
  133. data/src/stylesheets/nitro_kit/components/button_group.css +53 -0
  134. data/src/stylesheets/nitro_kit/components/button_to.css +6 -0
  135. data/src/stylesheets/nitro_kit/components/card.css +70 -0
  136. data/src/stylesheets/nitro_kit/components/checkbox.css +160 -0
  137. data/src/stylesheets/nitro_kit/components/checkbox_group.css +61 -0
  138. data/src/stylesheets/nitro_kit/components/combobox.css +148 -0
  139. data/src/stylesheets/nitro_kit/components/command_palette.css +255 -0
  140. data/src/stylesheets/nitro_kit/components/container.css +25 -0
  141. data/src/stylesheets/nitro_kit/components/control_group.css +149 -0
  142. data/src/stylesheets/nitro_kit/components/danger_zone.css +54 -0
  143. data/src/stylesheets/nitro_kit/components/data_section.css +40 -0
  144. data/src/stylesheets/nitro_kit/components/details_table.css +53 -0
  145. data/src/stylesheets/nitro_kit/components/dialog.css +80 -0
  146. data/src/stylesheets/nitro_kit/components/dropdown.css +152 -0
  147. data/src/stylesheets/nitro_kit/components/dropzone.css +198 -0
  148. data/src/stylesheets/nitro_kit/components/empty_state.css +53 -0
  149. data/src/stylesheets/nitro_kit/components/field.css +79 -0
  150. data/src/stylesheets/nitro_kit/components/field_group.css +6 -0
  151. data/src/stylesheets/nitro_kit/components/fieldset.css +28 -0
  152. data/src/stylesheets/nitro_kit/components/flex.css +455 -0
  153. data/src/stylesheets/nitro_kit/components/form_section.css +71 -0
  154. data/src/stylesheets/nitro_kit/components/grid.css +308 -0
  155. data/src/stylesheets/nitro_kit/components/icon.css +26 -0
  156. data/src/stylesheets/nitro_kit/components/input.css +80 -0
  157. data/src/stylesheets/nitro_kit/components/label.css +9 -0
  158. data/src/stylesheets/nitro_kit/components/layout.css +440 -0
  159. data/src/stylesheets/nitro_kit/components/page_header.css +61 -0
  160. data/src/stylesheets/nitro_kit/components/pagination.css +119 -0
  161. data/src/stylesheets/nitro_kit/components/pagination_bar.css +41 -0
  162. data/src/stylesheets/nitro_kit/components/palette.css +113 -0
  163. data/src/stylesheets/nitro_kit/components/progressive_image.css +139 -0
  164. data/src/stylesheets/nitro_kit/components/radio_button.css +127 -0
  165. data/src/stylesheets/nitro_kit/components/radio_button_group.css +143 -0
  166. data/src/stylesheets/nitro_kit/components/rich_text_area.css +21 -0
  167. data/src/stylesheets/nitro_kit/components/select.css +70 -0
  168. data/src/stylesheets/nitro_kit/components/settings_layout.css +100 -0
  169. data/src/stylesheets/nitro_kit/components/sheet.css +100 -0
  170. data/src/stylesheets/nitro_kit/components/stat_grid.css +51 -0
  171. data/src/stylesheets/nitro_kit/components/switch.css +152 -0
  172. data/src/stylesheets/nitro_kit/components/table.css +116 -0
  173. data/src/stylesheets/nitro_kit/components/tabs.css +138 -0
  174. data/src/stylesheets/nitro_kit/components/textarea.css +39 -0
  175. data/src/stylesheets/nitro_kit/components/toast.css +131 -0
  176. data/src/stylesheets/nitro_kit/components/toolbar.css +78 -0
  177. data/src/stylesheets/nitro_kit/components/tooltip.css +124 -0
  178. data/src/stylesheets/nitro_kit/components/typeset.css +191 -0
  179. data/src/stylesheets/nitro_kit/layers.css +7 -0
  180. data/src/stylesheets/nitro_kit/reset.css +192 -0
  181. data/src/stylesheets/nitro_kit/tokens.css +198 -0
  182. metadata +182 -51
  183. data/Rakefile +0 -8
  184. data/app/assets/tailwind/application.css +0 -49
  185. data/app/components/nitro_kit/datepicker.rb +0 -9
  186. data/app/helpers/nitro_kit/accordion_helper.rb +0 -9
  187. data/app/helpers/nitro_kit/alert_helper.rb +0 -11
  188. data/app/helpers/nitro_kit/avatar_helper.rb +0 -13
  189. data/app/helpers/nitro_kit/badge_helper.rb +0 -13
  190. data/app/helpers/nitro_kit/button_group_helper.rb +0 -9
  191. data/app/helpers/nitro_kit/button_helper.rb +0 -49
  192. data/app/helpers/nitro_kit/card_helper.rb +0 -9
  193. data/app/helpers/nitro_kit/checkbox_helper.rb +0 -50
  194. data/app/helpers/nitro_kit/combobox_helper.rb +0 -9
  195. data/app/helpers/nitro_kit/datepicker_helper.rb +0 -9
  196. data/app/helpers/nitro_kit/dialog_helper.rb +0 -9
  197. data/app/helpers/nitro_kit/dropdown_helper.rb +0 -9
  198. data/app/helpers/nitro_kit/field_group_helper.rb +0 -9
  199. data/app/helpers/nitro_kit/field_helper.rb +0 -9
  200. data/app/helpers/nitro_kit/fieldset_helper.rb +0 -9
  201. data/app/helpers/nitro_kit/form_helper.rb +0 -13
  202. data/app/helpers/nitro_kit/icon_helper.rb +0 -9
  203. data/app/helpers/nitro_kit/input_helper.rb +0 -37
  204. data/app/helpers/nitro_kit/label_helper.rb +0 -19
  205. data/app/helpers/nitro_kit/pagination_helper.rb +0 -46
  206. data/app/helpers/nitro_kit/radio_button_helper.rb +0 -23
  207. data/app/helpers/nitro_kit/select_helper.rb +0 -24
  208. data/app/helpers/nitro_kit/switch_helper.rb +0 -9
  209. data/app/helpers/nitro_kit/table_helper.rb +0 -9
  210. data/app/helpers/nitro_kit/tabs_helper.rb +0 -9
  211. data/app/helpers/nitro_kit/textarea_helper.rb +0 -9
  212. data/app/helpers/nitro_kit/toast_helper.rb +0 -25
  213. data/app/helpers/nitro_kit/tooltip_helper.rb +0 -9
  214. data/app/javascript/controllers/nk/accordion_controller.js +0 -18
  215. data/app/javascript/controllers/nk/datepicker_controller.js +0 -5
  216. data/app/javascript/controllers/nk/switch_controller.js +0 -32
  217. data/lib/generators/nitro_kit/component_generator.rb +0 -84
  218. data/lib/nitro_kit/schema_builder.rb +0 -129
  219. data/lib/nitro_kit/variants.rb +0 -21
@@ -0,0 +1,345 @@
1
+ require "pathname"
2
+ require "ripper"
3
+
4
+ module NitroKit
5
+ class MigrationInventory
6
+ Finding = Struct.new(:status, :path, :guidance, keyword_init: true)
7
+
8
+ LEGACY_COMPONENTS = %w[
9
+ accordion alert avatar avatar_stack badge button button_group card checkbox
10
+ checkbox_group combobox component datepicker dialog dropdown field field_group
11
+ fieldset form_builder icon input label pagination radio_button radio_button_group
12
+ select switch table tabs textarea toast tooltip
13
+ ].freeze
14
+ LEGACY_CONTROLLERS = %w[
15
+ accordion combobox datepicker dialog dropdown switch tabs toast tooltip
16
+ ].freeze
17
+ HELPER_REPLACEMENTS = {
18
+ "nk_button_link_to" => "replace with Button(..., href:)",
19
+ "nk_button_to" => "replace with ButtonTo(..., href:, method:)",
20
+ "nk_form_with" => "replace with Rails form_with(..., builder: NitroKit::FormBuilder)",
21
+ "nk_form_for" => "replace with Rails form_with(..., builder: NitroKit::FormBuilder)",
22
+ "nk_pagy_nav" => "replace with Pagination(pagy:)",
23
+ "nk_toast_flash_messages" => "replace with NitroKit::Toast::FlashMessages",
24
+ "nk_toast_turbo_stream_refresh" => "append NitroKit::Toast::Item to the toast list",
25
+ "nk_toast_action" => "replace the client toast action with Rails flash or a rendered Toast::Item",
26
+ "nk_checkbox_tag" => "replace with Checkbox(...) and preserve the submitted name and values",
27
+ "nk_check_box_tag" => "replace with Checkbox(...) and preserve the submitted name and values"
28
+ }.freeze
29
+ DIRECT_HELPERS = (LEGACY_COMPONENTS - %w[component datepicker form_builder]).freeze
30
+ FIELD_TAG_REPLACEMENTS = {
31
+ "color" => :color,
32
+ "date" => :date,
33
+ "datetime" => :"datetime-local",
34
+ "datetime_local" => :"datetime-local",
35
+ "email" => :email,
36
+ "file" => :file,
37
+ "hidden" => :hidden,
38
+ "month" => :month,
39
+ "number" => :number,
40
+ "password" => :password,
41
+ "phone" => :tel,
42
+ "range" => :range,
43
+ "search" => :search,
44
+ "telephone" => :tel,
45
+ "text" => :text,
46
+ "time" => :time,
47
+ "url" => :url,
48
+ "week" => :week
49
+ }.transform_keys { "nk_#{_1}_field_tag" }.freeze
50
+ VENDORED_MODULES = {
51
+ "@floating-ui/core" => "remove the 1.x Floating UI pin or package after copied controllers are removed",
52
+ "@floating-ui/dom" => "remove the 1.x Floating UI pin or package after copied controllers are removed",
53
+ "@floating-ui/utils" => "remove the 1.x Floating UI pin or package after copied controllers are removed",
54
+ "@github/combobox-nav" => "remove the 1.x combobox package; Nitro Kit 2 owns combobox behavior"
55
+ }.freeze
56
+
57
+ attr_reader :application_root
58
+
59
+ def initialize(application_root)
60
+ @application_root = Pathname.new(application_root)
61
+ end
62
+
63
+ def categories
64
+ {
65
+ "Legacy helpers" => legacy_helpers,
66
+ "Copied or shadow components" => copied_components,
67
+ "Custom or legacy controllers" => controllers,
68
+ "Vendored dependencies" => vendored_dependencies,
69
+ "Known replacements" => replacement_summary,
70
+ "Unresolved or application-owned items" => disposition_summary
71
+ }
72
+ end
73
+
74
+ private
75
+ def legacy_helpers
76
+ @legacy_helpers ||= source_files.flat_map do |path|
77
+ helper_identifiers(path).filter_map do |helper, line_number|
78
+ next unless legacy_helper?(helper)
79
+
80
+ Finding.new(
81
+ status: helper == "nk_datepicker" ? :application_owned : :unresolved,
82
+ path: location(path, line_number),
83
+ guidance: helper_guidance(helper)
84
+ )
85
+ end
86
+ end.uniq { [ _1.status, _1.path, _1.guidance ] }
87
+ end
88
+
89
+ def copied_components
90
+ application_root.glob("app/components/nitro_kit/**/*.rb").map do |path|
91
+ name = path.basename(".rb").to_s
92
+ known = legacy_component?(path, name)
93
+
94
+ Finding.new(
95
+ status: known && name != "datepicker" ? :unresolved : :application_owned,
96
+ path: relative(path),
97
+ guidance: component_guidance(name, known:)
98
+ )
99
+ end
100
+ end
101
+
102
+ def controllers
103
+ application_root.glob("app/javascript/controllers/nk/**/*_controller.js").map do |path|
104
+ name = path.basename("_controller.js").to_s
105
+ expected_path = "app/javascript/controllers/nk/#{name}_controller.js"
106
+ known = LEGACY_CONTROLLERS.include?(name) && relative(path) == expected_path
107
+
108
+ Finding.new(
109
+ status: known && name != "datepicker" ? :unresolved : :application_owned,
110
+ path: relative(path),
111
+ guidance: controller_guidance(name, known:)
112
+ )
113
+ end
114
+ end
115
+
116
+ def vendored_dependencies
117
+ findings = application_root.glob("vendor/javascript/{@floating-ui--*,@github--combobox-nav*}.js").map do |path|
118
+ Finding.new(
119
+ status: :unresolved,
120
+ path: relative(path),
121
+ guidance: "delete the vendored 1.x runtime after removing copied Nitro controllers"
122
+ )
123
+ end
124
+
125
+ dependency_files.each do |path|
126
+ path.each_line.with_index(1) do |line, line_number|
127
+ next if line.lstrip.start_with?("#", "//")
128
+
129
+ VENDORED_MODULES.each do |name, guidance|
130
+ findings << Finding.new(status: :unresolved, path: location(path, line_number), guidance:) if line.include?(name)
131
+ end
132
+ if line.match?(/\btailwind_merge\b/)
133
+ findings << Finding.new(
134
+ status: :unresolved,
135
+ path: location(path, line_number),
136
+ guidance: "remove tailwind_merge; Nitro Kit 2 ships static CSS and does not use it"
137
+ )
138
+ end
139
+ end
140
+ end
141
+
142
+ findings.uniq { [ _1.path, _1.guidance ] }
143
+ end
144
+
145
+ def replacement_summary
146
+ unresolved_findings.group_by(&:guidance).map do |guidance, findings|
147
+ Finding.new(
148
+ status: :unresolved,
149
+ path: "#{findings.size} occurrence#{"s" unless findings.one?}",
150
+ guidance:
151
+ )
152
+ end
153
+ end
154
+
155
+ def disposition_summary
156
+ all_findings.group_by(&:status).map do |status, findings|
157
+ guidance = case status
158
+ when :application_owned
159
+ "preserve the behavior under an application namespace; do not keep a Nitro shadow"
160
+ else
161
+ "apply the per-file replacement or removal listed in the category above"
162
+ end
163
+ Finding.new(
164
+ status:,
165
+ path: "#{findings.size} finding#{"s" unless findings.one?}",
166
+ guidance:
167
+ )
168
+ end
169
+ end
170
+
171
+ def unresolved_findings
172
+ all_findings.select { _1.status == :unresolved }
173
+ end
174
+
175
+ def all_findings
176
+ @all_findings ||= (legacy_helpers + copied_components + controllers + vendored_dependencies)
177
+ .uniq { [ _1.status, _1.path, _1.guidance ] }
178
+ end
179
+
180
+ def source_files
181
+ application_root.glob("app/**/*.{rb,erb,haml,slim}").select(&:file?)
182
+ end
183
+
184
+ def dependency_files
185
+ files = %w[Gemfile config/importmap.rb package.json].filter_map do |name|
186
+ path = application_root.join(name)
187
+ path if path.file?
188
+ end
189
+ files + application_root.glob("app/javascript/controllers/nk/**/*.js")
190
+ end
191
+
192
+ def helper_identifiers(path)
193
+ case path.extname
194
+ when ".rb"
195
+ ruby_identifiers(path.read)
196
+ when ".erb"
197
+ erb_identifiers(path.read)
198
+ when ".haml", ".slim"
199
+ template_line_identifiers(path.read)
200
+ else
201
+ []
202
+ end
203
+ end
204
+
205
+ def ruby_identifiers(source, line_offset = 0)
206
+ tokens = Ripper.lex(source)
207
+ tokens.each_index.filter_map do |index|
208
+ position, event, token, = tokens[index]
209
+ next unless event == :on_ident && token.start_with?("nk_")
210
+
211
+ previous = tokens[0...index].reverse.find { !%i[on_sp on_nl on_ignored_nl].include?(_1[1]) }
212
+ [ token, position.first + line_offset ] unless previous&.at(1) == :on_symbeg
213
+ end
214
+ end
215
+
216
+ def erb_identifiers(source)
217
+ source.to_enum(:scan, /<%(?!%|#)[=-]?(.*?)%>/m).flat_map do
218
+ match = Regexp.last_match
219
+ line_offset = source[0...match.begin(1)].count("\n")
220
+ ruby_identifiers(match[1], line_offset)
221
+ end
222
+ end
223
+
224
+ def template_line_identifiers(source)
225
+ source.each_line.with_index(1).flat_map do |line, line_number|
226
+ code = line.lstrip
227
+ next [] unless code.start_with?("=", "-") && !code.start_with?("-#")
228
+
229
+ ruby_identifiers(code.delete_prefix(code[0]), line_number - 1)
230
+ end
231
+ end
232
+
233
+ def legacy_component?(path, name)
234
+ return false unless LEGACY_COMPONENTS.include?(name)
235
+ return false unless relative(path) == "app/components/nitro_kit/#{name}.rb"
236
+
237
+ declared_constants(path.read).include?("NitroKit::#{camelize(name)}")
238
+ end
239
+
240
+ def declared_constants(source)
241
+ constants = []
242
+ walk_constants(Ripper.sexp(source), nil, constants)
243
+ constants
244
+ end
245
+
246
+ def walk_constants(node, namespace, constants)
247
+ return unless node.is_a?(Array)
248
+
249
+ if %i[module class].include?(node.first)
250
+ name = constant_name(node[1])
251
+ if name
252
+ full_name = name.include?("::") || namespace.nil? ? name : "#{namespace}::#{name}"
253
+ constants << full_name
254
+ walk_constants(node[node.first == :class ? 3 : 2], full_name, constants)
255
+ return
256
+ end
257
+ end
258
+
259
+ node.each { walk_constants(_1, namespace, constants) if _1.is_a?(Array) }
260
+ end
261
+
262
+ def constant_name(node)
263
+ return unless node.is_a?(Array)
264
+
265
+ case node.first
266
+ when :const_ref, :var_ref, :top_const_ref
267
+ constant_name(node[1])
268
+ when :const_path_ref
269
+ [ constant_name(node[1]), constant_name(node[2]) ].compact.join("::")
270
+ when :@const
271
+ node[1]
272
+ end
273
+ end
274
+
275
+ def legacy_helper?(helper)
276
+ HELPER_REPLACEMENTS.key?(helper) || FIELD_TAG_REPLACEMENTS.key?(helper) || helper == "nk_datepicker" ||
277
+ DIRECT_HELPERS.include?(helper.delete_prefix("nk_")) || automatic_variant_helper?(helper)
278
+ end
279
+
280
+ def automatic_variant_helper?(helper)
281
+ helper.match?(/\Ank_[a-z0-9]+_(?:alert|badge|button|button_to|button_link_to)\z/)
282
+ end
283
+
284
+ def helper_guidance(helper)
285
+ return HELPER_REPLACEMENTS.fetch(helper) if HELPER_REPLACEMENTS.key?(helper)
286
+ if FIELD_TAG_REPLACEMENTS.key?(helper)
287
+ type = FIELD_TAG_REPLACEMENTS.fetch(helper).inspect
288
+ return "replace with Input(type: #{type}) and preserve the submitted name and value"
289
+ end
290
+ if helper == "nk_datepicker"
291
+ return "use Input(type: :date) only when native date semantics are equivalent; otherwise preserve the behavior in an application-owned component"
292
+ end
293
+
294
+ if helper.end_with?("_button_link_to")
295
+ return "replace #{helper} with Button(..., href:) and pass the old variant explicitly"
296
+ end
297
+ if helper.end_with?("_button_to")
298
+ return "replace #{helper} with ButtonTo(..., href:, method:) and pass the old variant explicitly"
299
+ end
300
+ if automatic_variant_helper?(helper)
301
+ component = helper.delete_prefix("nk_").sub(/\A[a-z0-9]+_/, "")
302
+ return "replace #{helper} with direct Phlex #{camelize(component)}(...) and review the old variant against the current contract"
303
+ end
304
+
305
+ component = helper.delete_prefix("nk_")
306
+ "replace #{helper} with direct Phlex #{camelize(component)}(...) composition"
307
+ end
308
+
309
+ def component_guidance(name, known:)
310
+ if name == "component" && known
311
+ "delete the copied base component; include NitroKit once in the application-owned Phlex base"
312
+ elsif name == "form_builder" && known
313
+ "delete the shadow and select the gem-owned NitroKit::FormBuilder through Rails form_with"
314
+ elsif name == "datepicker" && known
315
+ "remove the 1.x copy; use Input(type: :date) only when equivalent, otherwise rebuild it in the application namespace"
316
+ elsif known
317
+ "delete the shadow and compose the gem-owned NitroKit::#{camelize(name)}"
318
+ else
319
+ "move this customization out of NitroKit into the application namespace and compose public Nitro components"
320
+ end
321
+ end
322
+
323
+ def controller_guidance(name, known:)
324
+ if name == "datepicker" && known
325
+ "remove the 1.x controller; keep specialized behavior only in an application-owned controller and component"
326
+ elsif known
327
+ "delete the 1.x controller and use the gem-owned Nitro behavior"
328
+ else
329
+ "move this custom behavior out of controllers/nk and register it under an application-owned identifier"
330
+ end
331
+ end
332
+
333
+ def camelize(value)
334
+ value.split("_").map(&:capitalize).join
335
+ end
336
+
337
+ def location(path, line_number)
338
+ "#{relative(path)}:#{line_number}"
339
+ end
340
+
341
+ def relative(path)
342
+ path.relative_path_from(application_root).to_s
343
+ end
344
+ end
345
+ end