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,403 @@
1
+ require "active_model"
2
+ require "nitro_kit"
3
+ require "set"
4
+
5
+ unless Rails.env.test?
6
+ raise "nitro_kit/upgrade_smoke_test may only be loaded in Rails test environment"
7
+ end
8
+
9
+ unless defined?(::ApplicationController)
10
+ raise "Nitro Kit upgrade smoke tests require the host ApplicationController"
11
+ end
12
+
13
+ module NitroKit
14
+ module UpgradeSmoke
15
+ PATH = "/_nitro_kit/upgrade_smoke"
16
+ FRAME_ID = "nitro-kit-upgrade-smoke"
17
+ SESSION_KEY = "nitro_kit_upgrade_smoke"
18
+
19
+ class Profile
20
+ include ActiveModel::Model
21
+ include ActiveModel::Attributes
22
+
23
+ attribute :name, :string
24
+
25
+ validates :name, presence: true, length: { minimum: 3 }
26
+
27
+ def persisted? = false
28
+ end
29
+
30
+ class Surface < Phlex::HTML
31
+ include Phlex::Rails::Helpers::FormWith
32
+ include Phlex::Rails::Helpers::TurboFrameTag
33
+
34
+ def initialize(profile:, revision:)
35
+ @profile = profile
36
+ @revision = revision
37
+ end
38
+
39
+ def view_template
40
+ section(data: { nitro_kit_upgrade_smoke: "surface" }) do
41
+ turbo_frame_tag(FRAME_ID) do
42
+ render_summary
43
+ render_form
44
+ render_dialog
45
+ render_sheet
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+ attr_reader :profile, :revision
52
+
53
+ def render_summary
54
+ render NitroKit::Card.new(id: "nitro-kit-upgrade-smoke-summary") do |card|
55
+ card.title("Post-mutation component", level: 2)
56
+ card.body do
57
+ p(data: { nitro_kit_upgrade_smoke_value: "name" }) { profile.name }
58
+ render NitroKit::Badge.new("Revision #{revision}", color: :info)
59
+ end
60
+ end
61
+ end
62
+
63
+ def render_form
64
+ form_with(
65
+ model: profile,
66
+ scope: :profile,
67
+ url: PATH,
68
+ method: :patch,
69
+ builder: NitroKit::FormBuilder,
70
+ id: "nitro-kit-upgrade-smoke-form",
71
+ data: { turbo_frame: "_top" }
72
+ ) do |form|
73
+ form.group do
74
+ form.field(:name, required: true, description: "This value must survive the mutation.")
75
+ form.submit("Save smoke-test profile", data: { turbo_submits_with: "Saving…" })
76
+ end
77
+ end
78
+ end
79
+
80
+ def render_dialog
81
+ render NitroKit::Dialog.new(id: "nitro-kit-upgrade-smoke-dialog") do |dialog|
82
+ dialog.trigger("Review dialog")
83
+ dialog.panel(
84
+ title: "Upgrade dialog",
85
+ description: "The native dialog contract rendered through the host application."
86
+ ) do
87
+ p { "Dialog content remains available after the upgrade." }
88
+ end
89
+ end
90
+ end
91
+
92
+ def render_sheet
93
+ render NitroKit::Sheet.new(id: "nitro-kit-upgrade-smoke-sheet", side: :right) do |sheet|
94
+ sheet.trigger("Review sheet")
95
+ sheet.panel(
96
+ title: "Upgrade sheet",
97
+ description: "The native sheet contract rendered through the host application."
98
+ ) do
99
+ p { "Sheet content remains available after the upgrade." }
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ class Controller < ::ApplicationController
106
+ def show
107
+ render_surface(profile: current_profile, revision: current_revision)
108
+ end
109
+
110
+ def update
111
+ profile = Profile.new(profile_params)
112
+
113
+ if profile.valid?
114
+ session[SESSION_KEY] = { "name" => profile.name, "revision" => current_revision + 1 }
115
+ redirect_to PATH, status: :see_other, notice: "Smoke-test profile saved"
116
+ else
117
+ flash.now[:alert] = "Smoke-test profile needs attention"
118
+ render_surface(profile:, revision: current_revision, status: :unprocessable_entity)
119
+ end
120
+ end
121
+
122
+ private
123
+ def current_state
124
+ session.fetch(SESSION_KEY, { "name" => "Original workspace", "revision" => 0 })
125
+ end
126
+
127
+ def current_profile
128
+ Profile.new(name: current_state.fetch("name"))
129
+ end
130
+
131
+ def current_revision
132
+ current_state.fetch("revision")
133
+ end
134
+
135
+ def profile_params
136
+ params.require(:profile).permit(:name)
137
+ end
138
+
139
+ def render_surface(profile:, revision:, status: :ok)
140
+ render Surface.new(profile:, revision:), status:
141
+ end
142
+ end
143
+
144
+ module RouteLifecycle
145
+ ROUTE_BLOCK = proc do
146
+ get PATH, to: Controller.action(:show)
147
+ patch PATH, to: Controller.action(:update)
148
+ end
149
+
150
+ @lock = Mutex.new
151
+ @owners = Set.new
152
+
153
+ class << self
154
+ def install!(owner)
155
+ ensure_test_environment!
156
+
157
+ @lock.synchronize do
158
+ return if @owners.include?(owner)
159
+
160
+ install_routes! if @owners.empty?
161
+ @owners.add(owner)
162
+ end
163
+ end
164
+
165
+ def uninstall!(owner)
166
+ @lock.synchronize do
167
+ @owners.delete(owner)
168
+ uninstall_routes! if @owners.empty? && route_block_installed?
169
+ end
170
+ end
171
+
172
+ private
173
+ def install_routes!
174
+ collisions = %w[GET PATCH].select { |method| route_matches?(method) }
175
+ if collisions.any?
176
+ raise "Nitro Kit upgrade smoke route #{PATH} collides with host #{collisions.join("/")} routing"
177
+ end
178
+
179
+ Rails.application.routes.append(&ROUTE_BLOCK)
180
+ Rails.application.reload_routes!
181
+ end
182
+
183
+ def uninstall_routes!
184
+ append_blocks.delete(ROUTE_BLOCK)
185
+ Rails.application.reload_routes!
186
+ end
187
+
188
+ def route_matches?(method)
189
+ request = ActionDispatch::Request.new(
190
+ Rack::MockRequest.env_for(PATH, method:)
191
+ )
192
+ Rails.application.routes.router.recognize(request) { return true }
193
+ false
194
+ end
195
+
196
+ def route_block_installed?
197
+ append_blocks.include?(ROUTE_BLOCK)
198
+ end
199
+
200
+ def append_blocks
201
+ Rails.application.routes.instance_variable_get(:@append)
202
+ end
203
+
204
+ def ensure_test_environment!
205
+ return if Rails.env.test?
206
+
207
+ raise "Nitro Kit upgrade smoke routes may only be installed in Rails test environment"
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ class UpgradeSmokeTest < ActionDispatch::IntegrationTest
214
+ setup { UpgradeSmoke::RouteLifecycle.install!(self) }
215
+ setup :prepare_nitro_kit_upgrade_smoke_test
216
+ teardown { UpgradeSmoke::RouteLifecycle.uninstall!(self) }
217
+
218
+ def self.runnable_methods
219
+ self == UpgradeSmokeTest ? [] : super
220
+ end
221
+
222
+ test "renders forms overlays flash and one stable Turbo Frame through Rails" do
223
+ get UpgradeSmoke::PATH
224
+
225
+ assert_response :success
226
+ assert_host_installation
227
+ assert_select "body section[data-nk='toast']", count: 1
228
+ assert_select "section[data-nitro-kit-upgrade-smoke='surface'] > turbo-frame##{UpgradeSmoke::FRAME_ID}", count: 1 do
229
+ assert_select "form#nitro-kit-upgrade-smoke-form[action='#{UpgradeSmoke::PATH}'][method='post'][data-turbo-frame='_top']" do
230
+ assert_select "input[name='_method'][value='patch']"
231
+ assert_select "input#profile_name[name='profile[name]'][required][data-nk='input']"
232
+ assert_select "label[data-slot='field-label'][for='profile_name']"
233
+ assert_select "button[type='submit'][data-turbo-submits-with='Saving…']"
234
+ end
235
+ assert_native_overlay("dialog", title: "Upgrade dialog")
236
+ assert_native_overlay("sheet", title: "Upgrade sheet")
237
+ end
238
+ end
239
+
240
+ test "keeps submitted values errors feedback and the frame boundary on an invalid mutation" do
241
+ patch UpgradeSmoke::PATH,
242
+ params: { profile: { name: "x" } }
243
+
244
+ assert_response :unprocessable_entity
245
+ assert_select "turbo-frame##{UpgradeSmoke::FRAME_ID}", count: 1 do
246
+ assert_select "input#profile_name[value='x'][aria-invalid='true'][aria-describedby~='profile_name-errors']"
247
+ assert_select "ul#profile_name-errors[data-slot='field-error'][aria-live='assertive']:not([role]) > li", count: 1
248
+ end
249
+ assert_select "section[data-nk='toast'] [data-nk='toast-item'][data-variant='error']"
250
+ end
251
+
252
+ test "redirects after mutation then renders the changed value and flash from Phlex" do
253
+ patch UpgradeSmoke::PATH, params: { profile: { name: "Migrated workspace" } }
254
+
255
+ assert_response :see_other
256
+ assert_redirected_to UpgradeSmoke::PATH
257
+
258
+ follow_redirect!
259
+
260
+ assert_response :success
261
+ assert_select "turbo-frame##{UpgradeSmoke::FRAME_ID}", count: 1 do
262
+ assert_select "article#nitro-kit-upgrade-smoke-summary[data-nk='card']" do
263
+ assert_select "[data-nitro-kit-upgrade-smoke-value='name']", text: "Migrated workspace"
264
+ assert_select "[data-nk='badge']", text: "Revision 1"
265
+ end
266
+ assert_select "input#profile_name[value='Migrated workspace']"
267
+ end
268
+ assert_select "section[data-nk='toast'] [data-nk='toast-item'][data-variant='default']"
269
+ end
270
+
271
+ private
272
+ def prepare_nitro_kit_upgrade_smoke_test
273
+ end
274
+
275
+ def assert_host_installation
276
+ assert_operator UpgradeSmoke::Controller, :<, ::ApplicationController
277
+ assert_select "html > head link[rel='stylesheet'][href*='nitro_kit']", minimum: 1
278
+ assert_select "html > head script[data-nk-appearance-default]", count: 1
279
+ assert_select "html > head script[type='importmap'], html > head script[type='module'][src], html > head script[defer][src]",
280
+ minimum: 1
281
+
282
+ bootstrap = css_select("html > head script[data-nk-appearance-default]").first
283
+ nonced_entrypoint = css_select("html > head script[nonce]").find { |script| script != bootstrap }
284
+ return unless nonced_entrypoint
285
+
286
+ assert_equal nonced_entrypoint["nonce"], bootstrap["nonce"]
287
+ end
288
+
289
+ def assert_native_overlay(component, title:)
290
+ root = "[data-nk='#{component}']"
291
+ panel_id = "nitro-kit-upgrade-smoke-#{component}-panel"
292
+
293
+ assert_select "#{root} [data-slot='#{component}-trigger'][command='show-modal'][commandfor='#{panel_id}']"
294
+ assert_select "#{root} dialog##{panel_id}[data-slot='#{component}-panel']" do
295
+ assert_select "[data-slot='#{component}-title']", text: title
296
+ assert_select "[data-slot='#{component}-close'][command='close'][commandfor='#{panel_id}']"
297
+ end
298
+ end
299
+ end
300
+
301
+ module UpgradeSmokeSystemTests
302
+ extend ActiveSupport::Concern
303
+
304
+ included do
305
+ setup { UpgradeSmoke::RouteLifecycle.install!(self) }
306
+ setup :prepare_nitro_kit_upgrade_smoke_test
307
+ teardown { UpgradeSmoke::RouteLifecycle.uninstall!(self) }
308
+
309
+ test "submits invalid and valid mutations through Turbo in the host application" do
310
+ visit UpgradeSmoke::PATH
311
+
312
+ assert_host_installation
313
+ track_turbo_submissions
314
+
315
+ find("#profile_name").set("x")
316
+ find("#nitro-kit-upgrade-smoke-form button[type='submit']").click
317
+
318
+ assert_selector "#profile_name[value='x'][aria-invalid='true'][aria-describedby~='profile_name-errors']"
319
+ assert_selector "ul#profile_name-errors[data-slot='field-error'][aria-live='assertive']:not([role]) > li", count: 1
320
+ assert_selector "body section[data-nk='toast'] [data-nk='toast-item'][data-variant='error']"
321
+ assert_stable_frame
322
+
323
+ find("#profile_name").set("Migrated workspace")
324
+ find("#nitro-kit-upgrade-smoke-form button[type='submit']").click
325
+
326
+ assert_selector "[data-nitro-kit-upgrade-smoke-value='name']", text: "Migrated workspace"
327
+ assert_selector "[data-nk='badge']", text: "Revision 1"
328
+ assert_selector "#profile_name[value='Migrated workspace']"
329
+ assert_selector "body section[data-nk='toast'] [data-nk='toast-item'][data-variant='default']"
330
+ assert_stable_frame
331
+ assert_equal 2, evaluate_script("window.__nitroKitUpgradeSmokeTurboSubmissions")
332
+ assert_no_unexpected_console_errors
333
+ end
334
+
335
+ test "opens and closes the native Dialog and Sheet in the host browser" do
336
+ visit UpgradeSmoke::PATH
337
+
338
+ exercise_overlay("dialog")
339
+ exercise_overlay("sheet")
340
+ end
341
+ end
342
+
343
+ private
344
+ def prepare_nitro_kit_upgrade_smoke_test
345
+ end
346
+
347
+ def assert_host_installation
348
+ assert_selector "html head link[rel='stylesheet'][href*='nitro_kit']", visible: :all
349
+ assert_selector "html head script[data-nk-appearance-default]", visible: :all
350
+ assert evaluate_script("typeof window.Turbo !== 'undefined'"), "Turbo did not load through the host JavaScript entrypoint"
351
+ end
352
+
353
+ def track_turbo_submissions
354
+ execute_script <<~JAVASCRIPT
355
+ window.__nitroKitUpgradeSmokeTurboSubmissions = 0
356
+ document.addEventListener("turbo:submit-end", () => {
357
+ window.__nitroKitUpgradeSmokeTurboSubmissions += 1
358
+ })
359
+ JAVASCRIPT
360
+ end
361
+
362
+ def assert_stable_frame
363
+ assert_selector "turbo-frame##{UpgradeSmoke::FRAME_ID}", count: 1
364
+ end
365
+
366
+ def assert_no_unexpected_console_errors
367
+ browser = page.driver.browser
368
+ return unless browser.respond_to?(:logs)
369
+
370
+ logs = browser.logs
371
+ return unless logs.available_types.map(&:to_s).include?("browser")
372
+
373
+ unexpected = logs.get(:browser).select { |entry| entry.level.to_s == "SEVERE" }.reject do |entry|
374
+ entry.message.include?(UpgradeSmoke::PATH) && entry.message.include?("422")
375
+ end
376
+ assert_empty unexpected, "Unexpected severe browser console errors: #{unexpected.map(&:message).join("\n")}"
377
+ rescue StandardError
378
+ # Browser log collection is driver-specific; interaction assertions remain authoritative.
379
+ end
380
+
381
+ def exercise_overlay(component)
382
+ root = "#nitro-kit-upgrade-smoke-#{component}"
383
+ panel = "#{root} [data-slot='#{component}-panel']"
384
+
385
+ find("#{root} [data-slot='#{component}-trigger']").click
386
+ assert_selector "#{panel}[open]"
387
+ assert_selector "#{panel} [data-slot='#{component}-title']", text: "Upgrade #{component}"
388
+
389
+ find("#{panel} [data-slot='#{component}-close']").click
390
+ assert_no_selector "#{panel}[open]"
391
+
392
+ find("#{root} [data-slot='#{component}-trigger']").click
393
+ execute_script <<~JAVASCRIPT, find(panel)
394
+ arguments[0].dispatchEvent(new MouseEvent("click", {
395
+ bubbles: true,
396
+ clientX: -1,
397
+ clientY: -1
398
+ }))
399
+ JAVASCRIPT
400
+ assert_no_selector "#{panel}[open]"
401
+ end
402
+ end
403
+ end
@@ -1,3 +1,3 @@
1
1
  module NitroKit
2
- VERSION = "0.9.0"
2
+ VERSION = "2.0.0.alpha.2"
3
3
  end
data/lib/nitro_kit.rb CHANGED
@@ -1,46 +1,9 @@
1
- require "tailwind_merge"
1
+ require "rails"
2
2
  require "phlex/rails"
3
3
 
4
- require "nitro_kit/version"
5
- require "nitro_kit/engine"
6
- require "nitro_kit/variants"
7
- require "nitro_kit/schema_builder"
8
-
9
4
  module NitroKit
10
- extend SchemaBuilder
11
-
12
- SCHEMA = build_schema do |s|
13
- s.add(:accordion, js: [ :accordion ])
14
- s.add(:alert)
15
- s.add(:avatar)
16
- s.add(:badge)
17
- s.add(:button, [ :icon ], components: [ :button, :button_group ], helpers: [ :button, :button_group ])
18
- s.add(:card)
19
- s.add(:checkbox, [ :label ], components: [ :checkbox, :checkbox_group ])
20
- s.add(
21
- :combobox,
22
- [ :input ],
23
- js: [ :combobox ],
24
- modules: [ "@floating-ui/core", "@floating-ui/dom", "@github/combobox-nav" ]
25
- )
26
- s.add(:datepicker)
27
- s.add(:dialog, [ :button, :icon ], js: [ :dialog ])
28
- s.add(:dropdown, [ :button ], js: [ :dropdown ], modules: [ "@floating-ui/core", "@floating-ui/dom" ])
29
- s.add(:field, [ :label, :checkbox, :combobox, :label, :radio_button, :select, :switch, :textarea ])
30
- s.add(:field_group)
31
- s.add(:fieldset, [ :field_group ])
32
- s.add(:form_builder, [ :field ], helpers: [ :form ])
33
- s.add(:icon, gems: [ "lucide-rails" ])
34
- s.add(:input)
35
- s.add(:label)
36
- s.add(:pagination, [ :icon, :button ])
37
- s.add(:radio_button, [ :label ], components: [ :radio_button, :radio_button_group ])
38
- s.add(:select)
39
- s.add(:switch, js: [ :switch ])
40
- s.add(:table)
41
- s.add(:tabs, js: [ :tabs ])
42
- s.add(:textarea)
43
- s.add(:toast, js: [ :toast ])
44
- s.add(:tooltip, js: [ :tooltip ], modules: [ "@floating-ui/core", "@floating-ui/dom" ])
45
- end
5
+ extend Phlex::Kit
46
6
  end
7
+
8
+ require "nitro_kit/version"
9
+ require "nitro_kit/engine"
@@ -0,0 +1,42 @@
1
+ require "rails/command"
2
+
3
+ module Rails
4
+ module Command
5
+ class NitroKitCommand < Base
6
+ desc "doctor", "Verify the Nitro Kit 2 application and agent integration"
7
+ def doctor
8
+ checks = installation.checks
9
+ checks.each do |check|
10
+ color = { pass: :green, warn: :yellow, fail: :red }.fetch(check.status)
11
+ say_status(check.status.to_s.upcase, "#{check.label}: #{check.detail}", color)
12
+ end
13
+
14
+ exit 1 if checks.any? { _1.status == :fail }
15
+ end
16
+
17
+ desc "prompt", "Print the Nitro Kit 2 initialization prompt"
18
+ option :copy, type: :boolean, default: false,
19
+ desc: "Copy the prompt to the clipboard"
20
+ def prompt
21
+ if options[:copy]
22
+ command = installation.copy_prompt
23
+ say "Copied the Nitro Kit 2 initialization prompt via #{command}."
24
+ else
25
+ say installation.prompt
26
+ end
27
+ rescue RuntimeError => error
28
+ say_error error.message
29
+ exit 1
30
+ end
31
+
32
+ private
33
+ def installation
34
+ @installation ||= begin
35
+ boot_application!
36
+ require "nitro_kit/installation"
37
+ NitroKit::Installation.new(Rails.root)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,4 +1,79 @@
1
- # desc "Explaining what the task does"
2
- # task :nitro_kit do
3
- # # Task goes here
4
- # end
1
+ require "fileutils"
2
+ require "pathname"
3
+ require "rake"
4
+
5
+ extend Rake::DSL
6
+
7
+ module NitroKit
8
+ module CssBundle
9
+ ROOT = Pathname.new(File.expand_path("../..", __dir__))
10
+ SOURCE_ROOT = ROOT.join("src/stylesheets/nitro_kit")
11
+ OUTPUT = ROOT.join("app/assets/stylesheets/nitro_kit.css")
12
+ FOUNDATION_SOURCES = %w[ layers.css tokens.css reset.css ].freeze
13
+ BANNER = <<~CSS.freeze
14
+ /*
15
+ * Nitro Kit 2.0
16
+ * Generated by `rake nitro_kit:css:build`. Do not edit directly.
17
+ */
18
+ CSS
19
+
20
+ module_function
21
+
22
+ def source_files
23
+ foundation_sources + component_sources
24
+ end
25
+
26
+ def compile
27
+ sections = source_files.map do |path|
28
+ <<~CSS.rstrip
29
+ /* Source: #{path.relative_path_from(ROOT)} */
30
+ #{path.read.rstrip}
31
+ CSS
32
+ end
33
+
34
+ ([ BANNER.rstrip ] + sections).join("\n\n") + "\n"
35
+ end
36
+
37
+ def write
38
+ css = compile
39
+
40
+ return false if OUTPUT.exist? && OUTPUT.read == css
41
+
42
+ FileUtils.mkdir_p(OUTPUT.dirname)
43
+ OUTPUT.write(css)
44
+ true
45
+ end
46
+
47
+ def current?
48
+ OUTPUT.exist? && OUTPUT.read == compile
49
+ end
50
+
51
+ def foundation_sources
52
+ FOUNDATION_SOURCES.map { |name| SOURCE_ROOT.join(name) }
53
+ end
54
+
55
+ def component_sources
56
+ SOURCE_ROOT.glob("components/**/*.css").sort
57
+ end
58
+ end
59
+ end
60
+
61
+ namespace :nitro_kit do
62
+ namespace :css do
63
+ desc "Build the committed Nitro Kit stylesheet"
64
+ task :build do
65
+ if NitroKit::CssBundle.write
66
+ puts "Wrote #{NitroKit::CssBundle::OUTPUT.relative_path_from(NitroKit::CssBundle::ROOT)}"
67
+ else
68
+ puts "Nitro Kit CSS is already up to date"
69
+ end
70
+ end
71
+
72
+ desc "Check that the committed Nitro Kit stylesheet is current"
73
+ task :check do
74
+ next if NitroKit::CssBundle.current?
75
+
76
+ abort "Nitro Kit CSS is out of date. Run `rake nitro_kit:css:build`."
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "nitro-kit",
3
+ "version": "0.2.0",
4
+ "description": "Build conventional Nitro Kit 2 Rails applications with Phlex and Hotwire.",
5
+ "author": {
6
+ "name": "Mikkel Malmberg",
7
+ "url": "https://nitrokit.dev"
8
+ },
9
+ "homepage": "https://nitrokit.dev",
10
+ "repository": "https://github.com/mikker/nitro_kit",
11
+ "license": "LicenseRef-NitroKit",
12
+ "keywords": ["rails", "phlex", "hotwire", "ui"],
13
+ "skills": "./skills/",
14
+ "interface": {
15
+ "displayName": "Nitro Kit",
16
+ "shortDescription": "Build Rails UI with Nitro Kit and Hotwire",
17
+ "longDescription": "Model conventional Rails resources, compose Nitro Kit components, and implement Hotwire interactions from the installed gem version.",
18
+ "developerName": "Mikkel Malmberg",
19
+ "category": "Developer",
20
+ "capabilities": ["Read", "Write"],
21
+ "websiteURL": "https://nitrokit.dev",
22
+ "defaultPrompt": [
23
+ "Build this Rails feature on the Nitro Kit Rails path.",
24
+ "Build this Rails interface with Nitro Kit components.",
25
+ "Implement this interaction with Nitro Kit and conventional Hotwire."
26
+ ]
27
+ }
28
+ }
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: nitro-kit-hotwire
3
+ description: Implement or refactor Nitro Kit 2 Hotwire interactions using conventional Turbo Drive, Frame, Stream, morph, form, dialog, flash, and toast patterns. Use for CRUD forms, validation, filtering, sorting, pagination, destructive confirmations, inline editing, self-submitting controls, server-rendered notifications, or any Nitro Kit interaction that should work progressively without copied controllers or custom request JavaScript.
4
+ ---
5
+
6
+ # Nitro Kit Hotwire
7
+
8
+ Make the server response and stable DOM boundary the interaction API. Add Stimulus only for behavior the browser, Rails, and Turbo cannot express.
9
+
10
+ ## Load the matching recipes
11
+
12
+ 1. Work from the Rails application root.
13
+ 2. Confirm `nitro_kit` appears in `Gemfile.lock`.
14
+ 3. Run `bundle show nitro_kit` and treat its output as `NITRO_KIT_ROOT`.
15
+ 4. Read `NITRO_KIT_ROOT/docs/agent_guide.md` completely.
16
+ 5. Read `NITRO_KIT_ROOT/docs/hotwire.md`, then only the matching files under `NITRO_KIT_ROOT/docs/patterns/`:
17
+ - query/filter/sort/paginate: `queryable_collection.md`
18
+ - create/update/validation: `resource_form.md`
19
+ - delete/revoke/archive confirmation: `destructive_action.md`
20
+ - notices/errors after responses: `flash_and_toast.md`
21
+ - row or panel edit/cancel: `inline_edit.md`
22
+ - application-wide confirmation and settings forms:
23
+ `application_foundation.md`
24
+ 6. Inspect the installed Nitro component contract used by the recipe. Never assume a recipe from another version still matches.
25
+
26
+ Do not proceed with a remembered Nitro Kit 1.x API. Do not copy or recreate
27
+ the installed gem's `nk--*` controllers under `app/javascript/controllers/nk`.
28
+
29
+ ## Choose the smallest Hotwire primitive
30
+
31
+ 1. Start with ordinary Rails navigation and forms under Turbo Drive.
32
+ 2. Add a Turbo Frame when one stable region has an independent navigation or form lifecycle.
33
+ 3. Return Turbo Streams over the request when one action must update multiple existing regions.
34
+ 4. Broadcast only when other sessions need the update.
35
+ 5. Add an application Stimulus controller only for client-only state or browser behavior not covered above.
36
+
37
+ Keep frames around complete resource or collection regions, not individual buttons. Derive IDs with Rails `dom_id` or a named constant and use the same identifier in links, forms, responses, and tests.
38
+
39
+ ## Keep response semantics conventional
40
+
41
+ - Redirect successful non-GET form submissions with `303 See Other`.
42
+ - Render invalid HTML and Turbo submissions with `422 Unprocessable Entity` and the same invalid model instance.
43
+ - Preserve an HTML response branch as the progressive fallback.
44
+ - Let GET query parameters be the source of truth for filtering, sorting, and pagination.
45
+ - Use native Nitro Dialog behavior for reviewed destructive actions. Use `data: { turbo_confirm: ... }` for compact confirmations that do not need a dialog.
46
+ - Render flash through `NitroKit::Toast::FlashMessages`; the application owns setting the flash.
47
+
48
+ ## Preserve ownership
49
+
50
+ - Nitro owns component markup, CSS, and its focused progressive controllers.
51
+ - Rails owns routes, records, authorization, query policy, validation, DOM identity, and responses.
52
+ - Hotwire owns transport and replacement.
53
+ - Application Stimulus owns only application-specific browser behavior.
54
+
55
+ Do not create a universal interaction controller, hide server behavior inside a UI component, or use Turbo Streams for a single replacement that a frame already handles.
56
+
57
+ ## Verify end to end
58
+
59
+ Cover the HTML fallback and Turbo response in request tests. Add a system test when focus, dialog behavior, frame navigation, or multiple DOM updates are central to the interaction. Assert response status and stable frame or target IDs, not Turbo internals.
60
+
61
+ Some headless browser sessions throttle `requestAnimationFrame`, which Turbo
62
+ uses before rendering a completed visit. If a request succeeds but the page or
63
+ frame never renders only in automation, reproduce it in a Rails system test
64
+ before changing application code. Disable Chrome's background throttling or
65
+ use a test-only `requestAnimationFrame` shim when necessary. Never ship that
66
+ workaround in the application or replace a conventional Turbo flow to satisfy
67
+ one browser driver.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Nitro Kit Hotwire"
3
+ short_description: "Build conventional Nitro Kit Hotwire flows"
4
+ default_prompt: "Use $nitro-kit-hotwire to build this interaction with Nitro Kit and conventional Hotwire."