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.
- checksums.yaml +4 -4
- data/.agents/plugins/marketplace.json +20 -0
- data/CHANGELOG.md +172 -0
- data/LICENSE +28 -0
- data/README.md +22 -15
- data/STYLE_GUIDE.md +425 -0
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
- data/app/assets/stylesheets/nitro_kit.css +7239 -0
- data/app/components/nitro_kit/accordion.rb +125 -65
- data/app/components/nitro_kit/alert.rb +80 -43
- data/app/components/nitro_kit/app_navigation.rb +307 -0
- data/app/components/nitro_kit/app_shell.rb +227 -0
- data/app/components/nitro_kit/appearance_bootstrap.rb +146 -0
- data/app/components/nitro_kit/appearance_picker.rb +156 -0
- data/app/components/nitro_kit/auth_shell.rb +34 -0
- data/app/components/nitro_kit/avatar.rb +85 -29
- data/app/components/nitro_kit/avatar_stack.rb +155 -9
- data/app/components/nitro_kit/badge.rb +43 -81
- data/app/components/nitro_kit/button.rb +180 -103
- data/app/components/nitro_kit/button_group.rb +63 -11
- data/app/components/nitro_kit/button_to.rb +69 -0
- data/app/components/nitro_kit/card.rb +42 -45
- data/app/components/nitro_kit/checkbox.rb +115 -62
- data/app/components/nitro_kit/checkbox_group.rb +131 -22
- data/app/components/nitro_kit/choice.rb +55 -0
- data/app/components/nitro_kit/combobox.rb +292 -105
- data/app/components/nitro_kit/command_palette.rb +412 -0
- data/app/components/nitro_kit/component.rb +322 -46
- data/app/components/nitro_kit/container.rb +32 -0
- data/app/components/nitro_kit/control_group.rb +45 -0
- data/app/components/nitro_kit/danger_zone.rb +78 -0
- data/app/components/nitro_kit/data_section.rb +106 -0
- data/app/components/nitro_kit/details_table.rb +229 -0
- data/app/components/nitro_kit/dialog.rb +257 -83
- data/app/components/nitro_kit/dropdown.rb +335 -122
- data/app/components/nitro_kit/dropzone.rb +326 -0
- data/app/components/nitro_kit/empty_state.rb +99 -0
- data/app/components/nitro_kit/field.rb +461 -236
- data/app/components/nitro_kit/field_group.rb +15 -8
- data/app/components/nitro_kit/fieldset.rb +37 -39
- data/app/components/nitro_kit/flex.rb +49 -0
- data/app/components/nitro_kit/form_builder.rb +313 -72
- data/app/components/nitro_kit/form_section.rb +79 -0
- data/app/components/nitro_kit/grid.rb +34 -0
- data/app/components/nitro_kit/icon.rb +54 -25
- data/app/components/nitro_kit/input.rb +97 -10
- data/app/components/nitro_kit/label.rb +22 -6
- data/app/components/nitro_kit/layout_options.rb +7 -0
- data/app/components/nitro_kit/page_header.rb +76 -0
- data/app/components/nitro_kit/pagination.rb +355 -69
- data/app/components/nitro_kit/pagination_bar.rb +90 -0
- data/app/components/nitro_kit/progressive_image.rb +181 -0
- data/app/components/nitro_kit/radio_button.rb +77 -55
- data/app/components/nitro_kit/radio_button_group.rb +107 -36
- data/app/components/nitro_kit/responsive_value.rb +94 -0
- data/app/components/nitro_kit/rich_text_area.rb +31 -0
- data/app/components/nitro_kit/select.rb +129 -64
- data/app/components/nitro_kit/settings_layout.rb +152 -0
- data/app/components/nitro_kit/sheet.rb +202 -0
- data/app/components/nitro_kit/stat_grid.rb +75 -0
- data/app/components/nitro_kit/switch.rb +108 -51
- data/app/components/nitro_kit/table.rb +237 -45
- data/app/components/nitro_kit/tabs.rb +168 -63
- data/app/components/nitro_kit/textarea.rb +81 -10
- data/app/components/nitro_kit/toast.rb +211 -80
- data/app/components/nitro_kit/toolbar.rb +75 -0
- data/app/components/nitro_kit/tooltip.rb +235 -34
- data/app/components/nitro_kit/typeset.rb +28 -0
- data/app/javascript/controllers/nk/app_shell_controller.js +224 -0
- data/app/javascript/controllers/nk/appearance_controller.js +73 -0
- data/app/javascript/controllers/nk/avatar_controller.js +15 -0
- data/app/javascript/controllers/nk/button_controller.js +56 -0
- data/app/javascript/controllers/nk/checkable_controller.js +41 -0
- data/app/javascript/controllers/nk/combobox_controller.js +314 -82
- data/app/javascript/controllers/nk/command_palette_controller.js +249 -0
- data/app/javascript/controllers/nk/dialog_controller.js +14 -10
- data/app/javascript/controllers/nk/dropdown_controller.js +106 -58
- data/app/javascript/controllers/nk/dropzone/direct_upload.js +61 -0
- data/app/javascript/controllers/nk/dropzone/file_rules.js +77 -0
- data/app/javascript/controllers/nk/dropzone/form_submit_lock.js +30 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +482 -0
- data/app/javascript/controllers/nk/overlay_position.js +37 -0
- data/app/javascript/controllers/nk/progressive_image_controller.js +116 -0
- data/app/javascript/controllers/nk/tabs_controller.js +135 -23
- data/app/javascript/controllers/nk/toast_controller.js +112 -45
- data/app/javascript/controllers/nk/tooltip_controller.js +12 -47
- data/config/importmap.rb +2 -0
- data/config/locales/en.yml +98 -0
- data/docs/agent_guide.md +139 -0
- data/docs/agent_native_spec.md +422 -0
- data/docs/component_contracts.md +263 -0
- data/docs/customization.md +595 -0
- data/docs/hotwire.md +94 -0
- data/docs/initialization_prompt.md +36 -0
- data/docs/migration_1_to_2.md +318 -0
- data/docs/new_app_strategy.md +22 -0
- data/docs/patterns/application_foundation.md +136 -0
- data/docs/patterns/crud_resource.md +144 -0
- data/docs/patterns/destructive_action.md +105 -0
- data/docs/patterns/flash_and_toast.md +57 -0
- data/docs/patterns/inline_edit.md +59 -0
- data/docs/patterns/queryable_collection.md +187 -0
- data/docs/patterns/resource_form.md +126 -0
- data/docs/rails_conventions.md +95 -0
- data/docs/rails_integration.md +514 -0
- data/lib/generators/nitro_kit/install_generator.rb +48 -0
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +67 -0
- data/lib/nitro_kit/engine.rb +20 -0
- data/lib/nitro_kit/installation.rb +671 -0
- data/lib/nitro_kit/migration_inventory.rb +345 -0
- data/lib/nitro_kit/upgrade_smoke_test.rb +403 -0
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/nitro_kit.rb +5 -42
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +42 -0
- data/lib/tasks/nitro_kit_tasks.rake +79 -4
- data/plugins/nitro-kit/.codex-plugin/plugin.json +28 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +67 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +60 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +62 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +4 -0
- data/src/stylesheets/nitro_kit/components/accordion.css +119 -0
- data/src/stylesheets/nitro_kit/components/alert.css +82 -0
- data/src/stylesheets/nitro_kit/components/app_navigation.css +156 -0
- data/src/stylesheets/nitro_kit/components/app_shell.css +605 -0
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +117 -0
- data/src/stylesheets/nitro_kit/components/auth_shell.css +8 -0
- data/src/stylesheets/nitro_kit/components/avatar.css +81 -0
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +80 -0
- data/src/stylesheets/nitro_kit/components/badge.css +76 -0
- data/src/stylesheets/nitro_kit/components/button.css +283 -0
- data/src/stylesheets/nitro_kit/components/button_group.css +53 -0
- data/src/stylesheets/nitro_kit/components/button_to.css +6 -0
- data/src/stylesheets/nitro_kit/components/card.css +70 -0
- data/src/stylesheets/nitro_kit/components/checkbox.css +160 -0
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +61 -0
- data/src/stylesheets/nitro_kit/components/combobox.css +148 -0
- data/src/stylesheets/nitro_kit/components/command_palette.css +255 -0
- data/src/stylesheets/nitro_kit/components/container.css +25 -0
- data/src/stylesheets/nitro_kit/components/control_group.css +149 -0
- data/src/stylesheets/nitro_kit/components/danger_zone.css +54 -0
- data/src/stylesheets/nitro_kit/components/data_section.css +40 -0
- data/src/stylesheets/nitro_kit/components/details_table.css +53 -0
- data/src/stylesheets/nitro_kit/components/dialog.css +80 -0
- data/src/stylesheets/nitro_kit/components/dropdown.css +152 -0
- data/src/stylesheets/nitro_kit/components/dropzone.css +198 -0
- data/src/stylesheets/nitro_kit/components/empty_state.css +53 -0
- data/src/stylesheets/nitro_kit/components/field.css +79 -0
- data/src/stylesheets/nitro_kit/components/field_group.css +6 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +28 -0
- data/src/stylesheets/nitro_kit/components/flex.css +455 -0
- data/src/stylesheets/nitro_kit/components/form_section.css +71 -0
- data/src/stylesheets/nitro_kit/components/grid.css +308 -0
- data/src/stylesheets/nitro_kit/components/icon.css +26 -0
- data/src/stylesheets/nitro_kit/components/input.css +80 -0
- data/src/stylesheets/nitro_kit/components/label.css +9 -0
- data/src/stylesheets/nitro_kit/components/layout.css +440 -0
- data/src/stylesheets/nitro_kit/components/page_header.css +61 -0
- data/src/stylesheets/nitro_kit/components/pagination.css +119 -0
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +41 -0
- data/src/stylesheets/nitro_kit/components/palette.css +113 -0
- data/src/stylesheets/nitro_kit/components/progressive_image.css +139 -0
- data/src/stylesheets/nitro_kit/components/radio_button.css +127 -0
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +143 -0
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +21 -0
- data/src/stylesheets/nitro_kit/components/select.css +70 -0
- data/src/stylesheets/nitro_kit/components/settings_layout.css +100 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +100 -0
- data/src/stylesheets/nitro_kit/components/stat_grid.css +51 -0
- data/src/stylesheets/nitro_kit/components/switch.css +152 -0
- data/src/stylesheets/nitro_kit/components/table.css +116 -0
- data/src/stylesheets/nitro_kit/components/tabs.css +138 -0
- data/src/stylesheets/nitro_kit/components/textarea.css +39 -0
- data/src/stylesheets/nitro_kit/components/toast.css +131 -0
- data/src/stylesheets/nitro_kit/components/toolbar.css +78 -0
- data/src/stylesheets/nitro_kit/components/tooltip.css +124 -0
- data/src/stylesheets/nitro_kit/components/typeset.css +191 -0
- data/src/stylesheets/nitro_kit/layers.css +7 -0
- data/src/stylesheets/nitro_kit/reset.css +192 -0
- data/src/stylesheets/nitro_kit/tokens.css +198 -0
- metadata +182 -51
- data/Rakefile +0 -8
- data/app/assets/tailwind/application.css +0 -49
- data/app/components/nitro_kit/datepicker.rb +0 -9
- data/app/helpers/nitro_kit/accordion_helper.rb +0 -9
- data/app/helpers/nitro_kit/alert_helper.rb +0 -11
- data/app/helpers/nitro_kit/avatar_helper.rb +0 -13
- data/app/helpers/nitro_kit/badge_helper.rb +0 -13
- data/app/helpers/nitro_kit/button_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/button_helper.rb +0 -49
- data/app/helpers/nitro_kit/card_helper.rb +0 -9
- data/app/helpers/nitro_kit/checkbox_helper.rb +0 -50
- data/app/helpers/nitro_kit/combobox_helper.rb +0 -9
- data/app/helpers/nitro_kit/datepicker_helper.rb +0 -9
- data/app/helpers/nitro_kit/dialog_helper.rb +0 -9
- data/app/helpers/nitro_kit/dropdown_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_helper.rb +0 -9
- data/app/helpers/nitro_kit/fieldset_helper.rb +0 -9
- data/app/helpers/nitro_kit/form_helper.rb +0 -13
- data/app/helpers/nitro_kit/icon_helper.rb +0 -9
- data/app/helpers/nitro_kit/input_helper.rb +0 -37
- data/app/helpers/nitro_kit/label_helper.rb +0 -19
- data/app/helpers/nitro_kit/pagination_helper.rb +0 -46
- data/app/helpers/nitro_kit/radio_button_helper.rb +0 -23
- data/app/helpers/nitro_kit/select_helper.rb +0 -24
- data/app/helpers/nitro_kit/switch_helper.rb +0 -9
- data/app/helpers/nitro_kit/table_helper.rb +0 -9
- data/app/helpers/nitro_kit/tabs_helper.rb +0 -9
- data/app/helpers/nitro_kit/textarea_helper.rb +0 -9
- data/app/helpers/nitro_kit/toast_helper.rb +0 -25
- data/app/helpers/nitro_kit/tooltip_helper.rb +0 -9
- data/app/javascript/controllers/nk/accordion_controller.js +0 -18
- data/app/javascript/controllers/nk/datepicker_controller.js +0 -5
- data/app/javascript/controllers/nk/switch_controller.js +0 -32
- data/lib/generators/nitro_kit/component_generator.rb +0 -84
- data/lib/nitro_kit/schema_builder.rb +0 -129
- 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
|
data/lib/nitro_kit/version.rb
CHANGED
data/lib/nitro_kit.rb
CHANGED
|
@@ -1,46 +1,9 @@
|
|
|
1
|
-
require "
|
|
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
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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.
|