nitro_kit 2.0.0.alpha.2 → 2.0.0.alpha.4
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/CHANGELOG.md +229 -1
- data/README.md +25 -10
- data/STYLE_GUIDE.md +47 -5
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +3 -3
- data/app/assets/stylesheets/nitro_kit.css +2820 -1816
- data/app/components/nitro_kit/accordion.rb +2 -26
- data/app/components/nitro_kit/alert.rb +1 -11
- data/app/components/nitro_kit/app_navigation.rb +45 -11
- data/app/components/nitro_kit/app_shell.rb +1 -7
- data/app/components/nitro_kit/appearance_picker.rb +11 -12
- data/app/components/nitro_kit/avatar.rb +6 -1
- data/app/components/nitro_kit/avatar_stack.rb +3 -0
- data/app/components/nitro_kit/badge.rb +13 -4
- data/app/components/nitro_kit/button_group.rb +2 -2
- data/app/components/nitro_kit/checkbox.rb +7 -4
- data/app/components/nitro_kit/checkbox_group.rb +0 -25
- data/app/components/nitro_kit/combobox.rb +4 -33
- data/app/components/nitro_kit/command_palette.rb +42 -59
- data/app/components/nitro_kit/component.rb +57 -5
- data/app/components/nitro_kit/danger_zone.rb +5 -1
- data/app/components/nitro_kit/data_section.rb +28 -4
- data/app/components/nitro_kit/details_table.rb +19 -8
- data/app/components/nitro_kit/dialog.rb +23 -19
- data/app/components/nitro_kit/dropdown.rb +1 -6
- data/app/components/nitro_kit/dropzone.rb +4 -15
- data/app/components/nitro_kit/empty_state.rb +21 -2
- data/app/components/nitro_kit/field.rb +35 -11
- data/app/components/nitro_kit/form_builder.rb +1 -2
- data/app/components/nitro_kit/progressive_image.rb +2 -2
- data/app/components/nitro_kit/radio_button.rb +7 -4
- data/app/components/nitro_kit/radio_button_group.rb +0 -25
- data/app/components/nitro_kit/select.rb +4 -17
- data/app/components/nitro_kit/settings_layout.rb +17 -4
- data/app/components/nitro_kit/{form_section.rb → settings_section.rb} +18 -11
- data/app/components/nitro_kit/sheet.rb +29 -20
- data/app/components/nitro_kit/stat_grid.rb +18 -2
- data/app/components/nitro_kit/table.rb +22 -3
- data/app/components/nitro_kit/tabs.rb +2 -13
- data/app/components/nitro_kit/toast.rb +4 -2
- data/app/components/nitro_kit/toolbar.rb +2 -0
- data/app/components/nitro_kit/tooltip.rb +7 -10
- data/app/javascript/controllers/nk/app_shell_controller.js +3 -3
- data/app/javascript/controllers/nk/appearance_controller.js +6 -0
- data/app/javascript/controllers/nk/combobox_controller.js +51 -1
- data/app/javascript/controllers/nk/command_palette_controller.js +25 -11
- data/app/javascript/controllers/nk/dialog_controller.js +54 -0
- data/app/javascript/controllers/nk/dropdown_controller.js +45 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +2 -2
- data/app/javascript/controllers/nk/progressive_image_controller.js +11 -7
- data/app/javascript/controllers/nk/tabs_controller.js +21 -2
- data/app/javascript/controllers/nk/tooltip_controller.js +9 -0
- data/config/locales/en.yml +4 -1
- data/docs/agent_guide.md +63 -123
- data/docs/agent_native_spec.md +72 -383
- data/docs/browser_support.md +82 -0
- data/docs/component_contracts.md +108 -82
- data/docs/customization.md +689 -91
- data/docs/hotwire.md +50 -76
- data/docs/initialization_prompt.md +26 -31
- data/docs/migration_1_to_2.md +88 -30
- data/docs/patterns/application_foundation.md +45 -119
- data/docs/patterns/crud_resource.md +44 -126
- data/docs/patterns/destructive_action.md +45 -77
- data/docs/patterns/flash_and_toast.md +18 -36
- data/docs/patterns/inline_edit.md +21 -44
- data/docs/patterns/queryable_collection.md +65 -161
- data/docs/patterns/resource_form.md +33 -81
- data/docs/rails_conventions.md +38 -75
- data/docs/rails_integration.md +96 -446
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +12 -0
- data/lib/nitro_kit/installation.rb +20 -7
- data/lib/nitro_kit/migration_inventory.rb +275 -8
- data/lib/nitro_kit/upgrade_smoke_test.rb +10 -12
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +10 -3
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +13 -1
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +32 -15
- data/src/stylesheets/nitro_kit/components/accordion.css +44 -44
- data/src/stylesheets/nitro_kit/components/alert.css +27 -36
- data/src/stylesheets/nitro_kit/components/app_navigation.css +92 -20
- data/src/stylesheets/nitro_kit/components/app_shell.css +199 -196
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +61 -39
- data/src/stylesheets/nitro_kit/components/auth_shell.css +2 -2
- data/src/stylesheets/nitro_kit/components/avatar.css +15 -11
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +48 -29
- data/src/stylesheets/nitro_kit/components/badge.css +13 -9
- data/src/stylesheets/nitro_kit/components/button.css +121 -79
- data/src/stylesheets/nitro_kit/components/button_group.css +19 -14
- data/src/stylesheets/nitro_kit/components/card.css +12 -6
- data/src/stylesheets/nitro_kit/components/checkbox.css +54 -67
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +16 -16
- data/src/stylesheets/nitro_kit/components/combobox.css +39 -29
- data/src/stylesheets/nitro_kit/components/command_palette.css +110 -51
- data/src/stylesheets/nitro_kit/components/container.css +6 -6
- data/src/stylesheets/nitro_kit/components/control_group.css +61 -67
- data/src/stylesheets/nitro_kit/components/danger_zone.css +8 -8
- data/src/stylesheets/nitro_kit/components/data_section.css +4 -4
- data/src/stylesheets/nitro_kit/components/details_table.css +17 -15
- data/src/stylesheets/nitro_kit/components/dialog.css +8 -5
- data/src/stylesheets/nitro_kit/components/dropdown.css +36 -30
- data/src/stylesheets/nitro_kit/components/dropzone.css +78 -35
- data/src/stylesheets/nitro_kit/components/empty_state.css +7 -7
- data/src/stylesheets/nitro_kit/components/field.css +32 -28
- data/src/stylesheets/nitro_kit/components/field_group.css +13 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +8 -2
- data/src/stylesheets/nitro_kit/components/flex.css +1 -1
- data/src/stylesheets/nitro_kit/components/grid.css +1 -1
- data/src/stylesheets/nitro_kit/components/icon.css +5 -5
- data/src/stylesheets/nitro_kit/components/input.css +42 -8
- data/src/stylesheets/nitro_kit/components/layout.css +165 -165
- data/src/stylesheets/nitro_kit/components/page_header.css +7 -7
- data/src/stylesheets/nitro_kit/components/pagination.css +58 -49
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +9 -9
- data/src/stylesheets/nitro_kit/components/palette.css +145 -77
- data/src/stylesheets/nitro_kit/components/progressive_image.css +45 -55
- data/src/stylesheets/nitro_kit/components/radio_button.css +48 -46
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +49 -49
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +16 -1
- data/src/stylesheets/nitro_kit/components/select.css +24 -15
- data/src/stylesheets/nitro_kit/components/settings_layout.css +26 -23
- data/src/stylesheets/nitro_kit/components/settings_section.css +84 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +30 -10
- data/src/stylesheets/nitro_kit/components/stat_grid.css +11 -10
- data/src/stylesheets/nitro_kit/components/switch.css +60 -62
- data/src/stylesheets/nitro_kit/components/table.css +43 -35
- data/src/stylesheets/nitro_kit/components/tabs.css +48 -40
- data/src/stylesheets/nitro_kit/components/textarea.css +20 -5
- data/src/stylesheets/nitro_kit/components/toast.css +11 -47
- data/src/stylesheets/nitro_kit/components/toolbar.css +16 -41
- data/src/stylesheets/nitro_kit/components/tooltip.css +38 -29
- data/src/stylesheets/nitro_kit/components/typeset.css +150 -4
- data/src/stylesheets/nitro_kit/reset.css +15 -15
- data/src/stylesheets/nitro_kit/tokens.css +501 -79
- metadata +4 -4
- data/docs/new_app_strategy.md +0 -22
- data/src/stylesheets/nitro_kit/components/form_section.css +0 -71
|
@@ -23,10 +23,18 @@ module NitroKit
|
|
|
23
23
|
Each skill resolves the installed gem with `bundle show nitro_kit` and reads
|
|
24
24
|
its version-matched documentation.
|
|
25
25
|
|
|
26
|
+
In a greenfield application, run `bin/rails generate phlex:install` and use
|
|
27
|
+
Phlex for the application layout, route views, and reusable UI. In an
|
|
28
|
+
established application, preserve its existing view architecture and
|
|
29
|
+
introduce Phlex and Nitro Kit only at the requested boundary unless an
|
|
30
|
+
application-wide migration is explicitly authorized.
|
|
31
|
+
|
|
26
32
|
Do not use Nitro Kit 1.x APIs, `nk_*` helpers, copied Nitro components, or
|
|
27
|
-
application-owned `controllers/nk`.
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
application-owned `controllers/nk`. Include `NitroKit` once in the base Phlex
|
|
34
|
+
component and prefer capitalized Kit methods such as `Button(...)` and
|
|
35
|
+
`Card(...)`; use `.new` only when another API requires a component object.
|
|
36
|
+
Keep routes, records, authorization, queries, DOM IDs, and server responses
|
|
37
|
+
in the application.
|
|
30
38
|
|
|
31
39
|
During migration, replace an existing form control only when Nitro Kit 2 has
|
|
32
40
|
a genuine semantic and behavioral equivalent. Otherwise preserve the control
|
|
@@ -152,12 +160,17 @@ module NitroKit
|
|
|
152
160
|
end
|
|
153
161
|
|
|
154
162
|
def stimulus_loader_check
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
javascript = application_root.glob("app/javascript/**/*.{js,ts}").select(&:file?).map(&:read)
|
|
164
|
+
loader = if javascript.any? { _1.include?("eagerLoadControllersFrom") }
|
|
165
|
+
"eager controller loading"
|
|
166
|
+
elsif javascript.any? { _1.match?(/\bapplication\.register\s*\(/) }
|
|
167
|
+
"explicit controller registration"
|
|
168
|
+
end
|
|
169
|
+
|
|
157
170
|
Check.new(
|
|
158
|
-
status:
|
|
171
|
+
status: loader ? :pass : :warn,
|
|
159
172
|
label: "Stimulus loader",
|
|
160
|
-
detail:
|
|
173
|
+
detail: loader ? "#{loader} is configured" : "verify the application's controller registration"
|
|
161
174
|
)
|
|
162
175
|
end
|
|
163
176
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "pathname"
|
|
2
|
+
require "prism"
|
|
2
3
|
require "ripper"
|
|
3
4
|
|
|
4
5
|
module NitroKit
|
|
@@ -53,6 +54,187 @@ module NitroKit
|
|
|
53
54
|
"@floating-ui/utils" => "remove the 1.x Floating UI pin or package after copied controllers are removed",
|
|
54
55
|
"@github/combobox-nav" => "remove the 1.x combobox package; Nitro Kit 2 owns combobox behavior"
|
|
55
56
|
}.freeze
|
|
57
|
+
BUTTON_TREATMENT_DEFINITION = /@utility\s+btn\b|\.btn(?:\s|[,{.:])/.freeze
|
|
58
|
+
BUTTON_CLASS = /(?<![a-z0-9_-])btn(?:-[a-z0-9_-]+)?(?![a-z0-9_-])/i.freeze
|
|
59
|
+
ERB_TAG_PATTERN = /<%(?!%|#)(==|[-=])?(.*?)(?:-)?%>/m
|
|
60
|
+
TABLE_COMPOUND_KEYWORDS = {
|
|
61
|
+
caption: %i[html aria data desperately_need_a_class],
|
|
62
|
+
thead: %i[html aria data desperately_need_a_class],
|
|
63
|
+
tbody: %i[html aria data desperately_need_a_class],
|
|
64
|
+
tr: %i[html aria data desperately_need_a_class],
|
|
65
|
+
th: %i[align scope sort href sort_data html aria data desperately_need_a_class],
|
|
66
|
+
td: %i[align html aria data desperately_need_a_class]
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
class ContractVisitor < Prism::Visitor
|
|
70
|
+
private
|
|
71
|
+
def positional_arguments(node)
|
|
72
|
+
Array(node.arguments&.arguments).reject { _1.is_a?(Prism::KeywordHashNode) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def keyword_names(node)
|
|
76
|
+
Array(node.arguments&.arguments).grep(Prism::KeywordHashNode).flat_map(&:elements).filter_map do |element|
|
|
77
|
+
element.key.unescaped.to_sym if element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def dynamic_arguments?(node)
|
|
82
|
+
Array(node.arguments&.arguments).any? do |argument|
|
|
83
|
+
argument.is_a?(Prism::SplatNode) || argument.is_a?(Prism::BlockArgumentNode) ||
|
|
84
|
+
(argument.is_a?(Prism::KeywordHashNode) && argument.elements.any? { _1.is_a?(Prism::AssocSplatNode) })
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def table_keyword_issues(node)
|
|
89
|
+
allowed = TABLE_COMPOUND_KEYWORDS[node.name]
|
|
90
|
+
return [] unless allowed
|
|
91
|
+
|
|
92
|
+
(keyword_names(node) - allowed).map do |keyword|
|
|
93
|
+
guidance = if keyword == :class
|
|
94
|
+
"Table##{node.name} does not accept class: directly; move it to desperately_need_a_class:"
|
|
95
|
+
elsif keyword == :style
|
|
96
|
+
"Table##{node.name} does not allow style overrides; preserve this as application-owned HTML"
|
|
97
|
+
else
|
|
98
|
+
"Table##{node.name} does not accept #{keyword}: directly; move it to html: { #{keyword}: ... }"
|
|
99
|
+
end
|
|
100
|
+
RuntimeContractVisitor::Issue.new(line: node.location.start_line, guidance:)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def receiver_named?(receiver, name)
|
|
105
|
+
receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == name ||
|
|
106
|
+
receiver.is_a?(Prism::CallNode) && receiver.receiver.nil? && receiver.name == name && receiver.arguments.nil?
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class RuntimeContractVisitor < ContractVisitor
|
|
111
|
+
Issue = Data.define(:line, :guidance)
|
|
112
|
+
|
|
113
|
+
attr_reader :issues
|
|
114
|
+
|
|
115
|
+
def initialize
|
|
116
|
+
@issues = []
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def visit_call_node(node)
|
|
120
|
+
inspect_button(node)
|
|
121
|
+
inspect_table(node)
|
|
122
|
+
inspect_table_local(node)
|
|
123
|
+
inspect_icon_triggers(node)
|
|
124
|
+
super
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
def inspect_button(node)
|
|
129
|
+
return unless node.name == :new && node.receiver&.respond_to?(:full_name)
|
|
130
|
+
return unless node.receiver.full_name == "NitroKit::Button"
|
|
131
|
+
return unless keyword_names(node).intersect?(%i[icon icon_end])
|
|
132
|
+
return if positional_arguments(node).any? { !_1.is_a?(Prism::NilNode) }
|
|
133
|
+
return if node.block || dynamic_arguments?(node)
|
|
134
|
+
return if keyword_names(node).intersect?(%i[label aria])
|
|
135
|
+
|
|
136
|
+
issues << Issue.new(
|
|
137
|
+
line: node.location.start_line,
|
|
138
|
+
guidance: "icon-only NitroKit::Button requires label:, aria: { label: ... }, or aria: { labelledby: ... }"
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def inspect_table(node)
|
|
143
|
+
constructor = positional_arguments(node).first
|
|
144
|
+
return unless node.name == :render && table_constructor?(constructor)
|
|
145
|
+
|
|
146
|
+
receiver_name = required_block_parameter_name(node)
|
|
147
|
+
return unless receiver_name
|
|
148
|
+
return if receiver_name == :table
|
|
149
|
+
|
|
150
|
+
visitor = TableCompoundVisitor.new(receiver_name)
|
|
151
|
+
visitor.visit(node.block.body) if node.block.body
|
|
152
|
+
issues.concat(visitor.issues)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def inspect_table_local(node)
|
|
156
|
+
return unless receiver_named?(node.receiver, :table)
|
|
157
|
+
|
|
158
|
+
issues.concat(table_keyword_issues(node))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def inspect_icon_triggers(node)
|
|
162
|
+
constructor = positional_arguments(node).first
|
|
163
|
+
return unless node.name == :render && node.block && constructor.is_a?(Prism::CallNode)
|
|
164
|
+
return unless constructor.name == :new && constructor.receiver&.respond_to?(:full_name)
|
|
165
|
+
|
|
166
|
+
component = constructor.receiver.full_name.delete_prefix("NitroKit::")
|
|
167
|
+
return unless %w[Dropdown Sheet].include?(component)
|
|
168
|
+
|
|
169
|
+
receiver_name = required_block_parameter_name(node)
|
|
170
|
+
return unless receiver_name
|
|
171
|
+
|
|
172
|
+
visitor = IconTriggerVisitor.new(receiver_name, component)
|
|
173
|
+
visitor.visit(node.block.body) if node.block.body
|
|
174
|
+
issues.concat(visitor.issues)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def required_block_parameter_name(node)
|
|
178
|
+
return unless node.block.is_a?(Prism::BlockNode)
|
|
179
|
+
|
|
180
|
+
parameter = node.block.parameters&.parameters&.requireds&.first
|
|
181
|
+
parameter.name if parameter.is_a?(Prism::RequiredParameterNode)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def table_constructor?(node)
|
|
185
|
+
node.is_a?(Prism::CallNode) && node.name == :new &&
|
|
186
|
+
node.receiver&.respond_to?(:full_name) && node.receiver.full_name == "NitroKit::Table"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
class IconTriggerVisitor < ContractVisitor
|
|
191
|
+
attr_reader :issues
|
|
192
|
+
|
|
193
|
+
def initialize(receiver_name, component)
|
|
194
|
+
@receiver_name = receiver_name
|
|
195
|
+
@component = component
|
|
196
|
+
@issues = []
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def visit_call_node(node)
|
|
200
|
+
if unnamed_icon_trigger?(node)
|
|
201
|
+
issues << RuntimeContractVisitor::Issue.new(
|
|
202
|
+
line: node.location.start_line,
|
|
203
|
+
guidance: "icon-only #{@component}#trigger requires label:, aria: { label: ... }, or aria: { labelledby: ... }"
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
super
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
def unnamed_icon_trigger?(node)
|
|
211
|
+
return false unless node.name == :trigger
|
|
212
|
+
return false unless node.receiver.is_a?(Prism::LocalVariableReadNode) && node.receiver.name == @receiver_name
|
|
213
|
+
return false unless keyword_names(node).intersect?(%i[icon icon_end])
|
|
214
|
+
return false if positional_arguments(node).any? { !_1.is_a?(Prism::NilNode) }
|
|
215
|
+
return false if node.block || dynamic_arguments?(node)
|
|
216
|
+
return false if keyword_names(node).intersect?(%i[label aria])
|
|
217
|
+
|
|
218
|
+
true
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
class TableCompoundVisitor < ContractVisitor
|
|
223
|
+
attr_reader :issues
|
|
224
|
+
|
|
225
|
+
def initialize(receiver_name)
|
|
226
|
+
@receiver_name = receiver_name
|
|
227
|
+
@issues = []
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def visit_call_node(node)
|
|
231
|
+
if receiver_named?(node.receiver, @receiver_name)
|
|
232
|
+
issues.concat(table_keyword_issues(node))
|
|
233
|
+
end
|
|
234
|
+
super
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
private_constant :ContractVisitor, :RuntimeContractVisitor, :IconTriggerVisitor, :TableCompoundVisitor
|
|
56
238
|
|
|
57
239
|
attr_reader :application_root
|
|
58
240
|
|
|
@@ -66,6 +248,8 @@ module NitroKit
|
|
|
66
248
|
"Copied or shadow components" => copied_components,
|
|
67
249
|
"Custom or legacy controllers" => controllers,
|
|
68
250
|
"Vendored dependencies" => vendored_dependencies,
|
|
251
|
+
"Application-owned button treatments" => application_button_treatments,
|
|
252
|
+
"2.0 runtime contract errors" => runtime_contract_errors,
|
|
69
253
|
"Known replacements" => replacement_summary,
|
|
70
254
|
"Unresolved or application-owned items" => disposition_summary
|
|
71
255
|
}
|
|
@@ -129,19 +313,98 @@ module NitroKit
|
|
|
129
313
|
VENDORED_MODULES.each do |name, guidance|
|
|
130
314
|
findings << Finding.new(status: :unresolved, path: location(path, line_number), guidance:) if line.include?(name)
|
|
131
315
|
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
316
|
end
|
|
140
317
|
end
|
|
141
318
|
|
|
142
319
|
findings.uniq { [ _1.path, _1.guidance ] }
|
|
143
320
|
end
|
|
144
321
|
|
|
322
|
+
def application_button_treatments
|
|
323
|
+
return [] unless application_stylesheets.any? { _1.read.match?(BUTTON_TREATMENT_DEFINITION) }
|
|
324
|
+
|
|
325
|
+
source_files.filter_map do |path|
|
|
326
|
+
lines = path.each_line.with_index(1).filter_map do |line, line_number|
|
|
327
|
+
next if line.lstrip.start_with?("#", "<%#")
|
|
328
|
+
|
|
329
|
+
line_number if line.match?(BUTTON_CLASS)
|
|
330
|
+
end
|
|
331
|
+
next if lines.empty?
|
|
332
|
+
|
|
333
|
+
Finding.new(
|
|
334
|
+
status: :application_owned,
|
|
335
|
+
path: "#{relative(path)}:#{lines.join(',')}",
|
|
336
|
+
guidance: "review the application-owned button treatment; migrate ordinary actions to NitroKit::Button and preserve specialized controls as semantic application HTML"
|
|
337
|
+
)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def runtime_contract_errors
|
|
342
|
+
source_files.flat_map do |path|
|
|
343
|
+
source = runtime_ruby_source(path)
|
|
344
|
+
next [] unless source
|
|
345
|
+
|
|
346
|
+
result = Prism.parse(source)
|
|
347
|
+
issues = if result.success?
|
|
348
|
+
runtime_contract_issues(result.value)
|
|
349
|
+
elsif path.extname == ".erb"
|
|
350
|
+
erb_runtime_contract_issues(path.read)
|
|
351
|
+
else
|
|
352
|
+
[]
|
|
353
|
+
end
|
|
354
|
+
issues.map do |issue|
|
|
355
|
+
Finding.new(
|
|
356
|
+
status: :unresolved,
|
|
357
|
+
path: location(path, issue.line),
|
|
358
|
+
guidance: issue.guidance
|
|
359
|
+
)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def runtime_contract_issues(node, line_offset: 0)
|
|
365
|
+
visitor = RuntimeContractVisitor.new
|
|
366
|
+
visitor.visit(node)
|
|
367
|
+
visitor.issues.map { _1.with(line: _1.line + line_offset) }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def erb_runtime_contract_issues(source)
|
|
371
|
+
source.to_enum(:scan, ERB_TAG_PATTERN).flat_map do
|
|
372
|
+
match = Regexp.last_match
|
|
373
|
+
fragment = match[2].sub(/\bdo(?:\s*\|[^|]*\|)?\s*\z/, "")
|
|
374
|
+
result = Prism.parse(fragment)
|
|
375
|
+
next [] unless result.success?
|
|
376
|
+
|
|
377
|
+
line_offset = source[0...match.begin(2)].count("\n")
|
|
378
|
+
runtime_contract_issues(result.value, line_offset:)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def runtime_ruby_source(path)
|
|
383
|
+
case path.extname
|
|
384
|
+
when ".rb" then path.read
|
|
385
|
+
when ".erb" then erb_ruby_source(path.read)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def erb_ruby_source(source)
|
|
390
|
+
ruby = source.gsub(/[^\n]/, " ")
|
|
391
|
+
source.to_enum(:scan, ERB_TAG_PATTERN).each do
|
|
392
|
+
match = Regexp.last_match
|
|
393
|
+
next if match[1]&.include?("=") && !standalone_erb_tag?(source, match)
|
|
394
|
+
|
|
395
|
+
ruby[match.begin(2)...match.end(2)] = match[2]
|
|
396
|
+
end
|
|
397
|
+
ruby
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def standalone_erb_tag?(source, match)
|
|
401
|
+
line_break = source.rindex("\n", match.begin(0) - 1)
|
|
402
|
+
line_start = line_break ? line_break + 1 : 0
|
|
403
|
+
line_end = source.index("\n", match.end(0)) || source.length
|
|
404
|
+
|
|
405
|
+
source[line_start...match.begin(0)].strip.empty? && source[match.end(0)...line_end].strip.empty?
|
|
406
|
+
end
|
|
407
|
+
|
|
145
408
|
def replacement_summary
|
|
146
409
|
unresolved_findings.group_by(&:guidance).map do |guidance, findings|
|
|
147
410
|
Finding.new(
|
|
@@ -173,7 +436,7 @@ module NitroKit
|
|
|
173
436
|
end
|
|
174
437
|
|
|
175
438
|
def all_findings
|
|
176
|
-
@all_findings ||= (legacy_helpers + copied_components + controllers + vendored_dependencies)
|
|
439
|
+
@all_findings ||= (legacy_helpers + copied_components + controllers + vendored_dependencies + application_button_treatments + runtime_contract_errors)
|
|
177
440
|
.uniq { [ _1.status, _1.path, _1.guidance ] }
|
|
178
441
|
end
|
|
179
442
|
|
|
@@ -181,6 +444,10 @@ module NitroKit
|
|
|
181
444
|
application_root.glob("app/**/*.{rb,erb,haml,slim}").select(&:file?)
|
|
182
445
|
end
|
|
183
446
|
|
|
447
|
+
def application_stylesheets
|
|
448
|
+
application_root.glob("app/**/*.{css,scss,sass}").select(&:file?)
|
|
449
|
+
end
|
|
450
|
+
|
|
184
451
|
def dependency_files
|
|
185
452
|
files = %w[Gemfile config/importmap.rb package.json].filter_map do |name|
|
|
186
453
|
path = application_root.join(name)
|
|
@@ -171,34 +171,32 @@ module NitroKit
|
|
|
171
171
|
|
|
172
172
|
private
|
|
173
173
|
def install_routes!
|
|
174
|
-
collisions = %w[GET PATCH].select { |method|
|
|
174
|
+
collisions = %w[GET PATCH].select { |method| exact_route_matches?(method) }
|
|
175
175
|
if collisions.any?
|
|
176
176
|
raise "Nitro Kit upgrade smoke route #{PATH} collides with host #{collisions.join("/")} routing"
|
|
177
177
|
end
|
|
178
178
|
|
|
179
|
-
Rails.application.routes.
|
|
179
|
+
Rails.application.routes.prepend(&ROUTE_BLOCK)
|
|
180
180
|
Rails.application.reload_routes!
|
|
181
181
|
end
|
|
182
182
|
|
|
183
183
|
def uninstall_routes!
|
|
184
|
-
|
|
184
|
+
prepend_blocks.delete(ROUTE_BLOCK)
|
|
185
185
|
Rails.application.reload_routes!
|
|
186
186
|
end
|
|
187
187
|
|
|
188
|
-
def
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
Rails.application.routes.router.recognize(request) { return true }
|
|
193
|
-
false
|
|
188
|
+
def exact_route_matches?(method)
|
|
189
|
+
Rails.application.routes.routes.any? do |route|
|
|
190
|
+
route.verb.to_s.match?(method) && route.path.spec.to_s.split("(", 2).first == PATH
|
|
191
|
+
end
|
|
194
192
|
end
|
|
195
193
|
|
|
196
194
|
def route_block_installed?
|
|
197
|
-
|
|
195
|
+
prepend_blocks.include?(ROUTE_BLOCK)
|
|
198
196
|
end
|
|
199
197
|
|
|
200
|
-
def
|
|
201
|
-
Rails.application.routes.instance_variable_get(:@
|
|
198
|
+
def prepend_blocks
|
|
199
|
+
Rails.application.routes.instance_variable_get(:@prepend)
|
|
202
200
|
end
|
|
203
201
|
|
|
204
202
|
def ensure_test_environment!
|
data/lib/nitro_kit/version.rb
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
require "json"
|
|
1
2
|
require "rails/command"
|
|
2
3
|
|
|
3
4
|
module Rails
|
|
4
5
|
module Command
|
|
5
6
|
class NitroKitCommand < Base
|
|
6
7
|
desc "doctor", "Verify the Nitro Kit 2 application and agent integration"
|
|
8
|
+
option :format, type: :string, default: "text", enum: %w[text json],
|
|
9
|
+
desc: "Output human-readable text or structured JSON"
|
|
7
10
|
def doctor
|
|
8
11
|
checks = installation.checks
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
if options[:format] == "json"
|
|
13
|
+
say JSON.pretty_generate(checks.map { { status: _1.status, label: _1.label, detail: _1.detail } })
|
|
14
|
+
else
|
|
15
|
+
checks.each do |check|
|
|
16
|
+
color = { pass: :green, warn: :yellow, fail: :red }.fetch(check.status)
|
|
17
|
+
say_status(check.status.to_s.upcase, "#{check.label}: #{check.detail}", color)
|
|
18
|
+
end
|
|
12
19
|
end
|
|
13
20
|
|
|
14
21
|
exit 1 if checks.any? { _1.status == :fail }
|
|
@@ -22,6 +22,8 @@ Make the server response and stable DOM boundary the interaction API. Add Stimul
|
|
|
22
22
|
- application-wide confirmation and settings forms:
|
|
23
23
|
`application_foundation.md`
|
|
24
24
|
6. Inspect the installed Nitro component contract used by the recipe. Never assume a recipe from another version still matches.
|
|
25
|
+
7. Read `NITRO_KIT_ROOT/docs/browser_support.md` for the canonical
|
|
26
|
+
full/reduced/unavailable no-JavaScript classification.
|
|
25
27
|
|
|
26
28
|
Do not proceed with a remembered Nitro Kit 1.x API. Do not copy or recreate
|
|
27
29
|
the installed gem's `nk--*` controllers under `app/javascript/controllers/nk`.
|
|
@@ -41,6 +43,8 @@ Keep frames around complete resource or collection regions, not individual butto
|
|
|
41
43
|
- Redirect successful non-GET form submissions with `303 See Other`.
|
|
42
44
|
- Render invalid HTML and Turbo submissions with `422 Unprocessable Entity` and the same invalid model instance.
|
|
43
45
|
- Preserve an HTML response branch as the progressive fallback.
|
|
46
|
+
- Do not describe that HTML branch as a JavaScript-free interaction when its
|
|
47
|
+
control still depends on Turbo or a closed overlay.
|
|
44
48
|
- Let GET query parameters be the source of truth for filtering, sorting, and pagination.
|
|
45
49
|
- Use native Nitro Dialog behavior for reviewed destructive actions. Use `data: { turbo_confirm: ... }` for compact confirmations that do not need a dialog.
|
|
46
50
|
- Render flash through `NitroKit::Toast::FlashMessages`; the application owns setting the flash.
|
|
@@ -21,12 +21,21 @@ established application outside the requested scope.
|
|
|
21
21
|
7. For authentication, teams, application navigation, or settings, read
|
|
22
22
|
`NITRO_KIT_ROOT/docs/patterns/application_foundation.md` completely.
|
|
23
23
|
8. Read the matching Hotwire recipe before implementing an interaction.
|
|
24
|
+
9. Read `NITRO_KIT_ROOT/docs/browser_support.md` before claiming an
|
|
25
|
+
interaction works without JavaScript.
|
|
24
26
|
|
|
25
27
|
Never use a Nitro Kit 1.x helper, copied component, controller, or Tailwind
|
|
26
28
|
contract as a substitute for the installed API.
|
|
27
29
|
|
|
28
30
|
## Follow the application grammar
|
|
29
31
|
|
|
32
|
+
- In a greenfield application, run `bin/rails generate phlex:install` and use
|
|
33
|
+
Phlex for the application layout, route-level views, and reusable UI. Render
|
|
34
|
+
`Views::*` objects from controllers instead of creating ERB wrappers around
|
|
35
|
+
Phlex. In an established application, preserve its view architecture and
|
|
36
|
+
introduce Phlex only at the requested boundary unless an application-wide
|
|
37
|
+
migration is explicitly authorized. Judge this from existing view code, not
|
|
38
|
+
the Rails version or apparent age.
|
|
30
39
|
- Scope tenant data through `Current.team` or `Current.account`; record
|
|
31
40
|
`Current.user` as the actor.
|
|
32
41
|
- Put domain behavior on the model before introducing another abstraction.
|
|
@@ -42,10 +51,13 @@ contract as a substitute for the installed API.
|
|
|
42
51
|
Nitro Kit 1.x source as the fallback.
|
|
43
52
|
- Redirect successful mutations with `303`; render invalid models with `422`.
|
|
44
53
|
- Render HTML on the server and add Hotwire progressively.
|
|
54
|
+
- Set the document language on the root `html` element.
|
|
45
55
|
- Test with Minitest and fixtures, including tenancy and unhappy paths.
|
|
46
56
|
- In authenticated admin areas, default to a hybrid `AppShell` with the route's
|
|
47
57
|
one `h1` and basic actions in its `Toolbar`. Keep one page gutter and avoid
|
|
48
|
-
repeated headings or automatic Card wrappers.
|
|
58
|
+
repeated headings or automatic Card wrappers. At narrow widths, let trailing
|
|
59
|
+
actions stack below a Back affordance and title instead of clipping the title
|
|
60
|
+
or hiding persistent actions.
|
|
49
61
|
- In a new team-aware application, create the first user's `Team` and owner
|
|
50
62
|
`Membership` together. Put roles on memberships and scope product records
|
|
51
63
|
through `Current.team`.
|
|
@@ -14,31 +14,45 @@ Use the documentation shipped with the application's installed gem as the source
|
|
|
14
14
|
3. Run `bundle show nitro_kit` and treat its output as `NITRO_KIT_ROOT`.
|
|
15
15
|
4. Read `NITRO_KIT_ROOT/docs/agent_guide.md` completely.
|
|
16
16
|
5. Read the relevant sections of `NITRO_KIT_ROOT/docs/component_contracts.md`. Read `customization.md` only for themes, tokens, or application composition.
|
|
17
|
-
6.
|
|
17
|
+
6. For any interactive component or no-JavaScript claim, read the canonical
|
|
18
|
+
classifications in `NITRO_KIT_ROOT/docs/browser_support.md`.
|
|
19
|
+
7. Inspect the installed component source when constructor or compound-slot details remain unclear. Never guess a component API from memory.
|
|
18
20
|
|
|
19
21
|
For a Nitro Kit 1.x migration, read
|
|
20
22
|
`NITRO_KIT_ROOT/docs/migration_1_to_2.md` before editing. Inventory product
|
|
21
|
-
flows
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
flows, behavior, application-owned button classes and Rails button helpers,
|
|
24
|
+
joined controls, and the existing semantic color, focus, radius, density, and
|
|
25
|
+
typography tokens first. Capture representative wide and narrow screenshots.
|
|
26
|
+
If the Nitro Kit MCP catalog is available, search it by workflow rather than
|
|
27
|
+
old component name, then select high-level compositions before replacing
|
|
28
|
+
atoms.
|
|
24
29
|
|
|
25
30
|
If the gem is not installed, say that the skill requires Nitro Kit and follow the application's requested installation scope. Do not substitute APIs from an older Nitro Kit release.
|
|
26
31
|
|
|
27
32
|
## Build the interface
|
|
28
33
|
|
|
29
|
-
1.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
1. In a greenfield application, run `bin/rails generate phlex:install` and use
|
|
35
|
+
Phlex for the application layout, route-level `Views::*`, and reusable UI.
|
|
36
|
+
Do not add ERB wrappers whose only purpose is to render Phlex. In an
|
|
37
|
+
established application with meaningful view conventions, preserve them
|
|
38
|
+
and introduce Phlex and Nitro Kit only at the requested boundary unless an
|
|
39
|
+
application-wide migration is explicitly authorized. Determine this from
|
|
40
|
+
the existing view architecture, not the Rails version or apparent age.
|
|
41
|
+
2. Reuse the highest-level Nitro block that matches the page region, then compose components inside it.
|
|
42
|
+
3. Include `NitroKit` once in the application's base Phlex component and use capitalized Kit methods such as `Button(...)` and `Card(...)`. Use `.new` only when another API needs a component object. Keep product-specific components under the application's namespace, commonly `UI::*`.
|
|
43
|
+
4. Use `NitroKit::FormBuilder` explicitly with Rails `form_with` for model-backed forms.
|
|
44
|
+
5. Keep routes, authorization, records, query policy, DOM IDs, Turbo boundaries, and response semantics in the application.
|
|
45
|
+
6. Translate the application's semantic theme into documented `--nk-*` properties instead of choosing similar raw palette values. Use `--nk-button-radius` when Button shape intentionally differs from inputs and surfaces.
|
|
46
|
+
7. Verify closed options and required compound declarations before rendering.
|
|
47
|
+
8. For authenticated CRUD, prefer a hybrid `AppShell` with a `Toolbar` that
|
|
36
48
|
owns the route's single `h1` and basic actions. The shell main region owns
|
|
37
49
|
one content gutter. Do not repeat that heading in `PageHeader`, or wrap each
|
|
38
|
-
table, form, and detail region in another Card.
|
|
39
|
-
|
|
50
|
+
table, form, and detail region in another Card. At narrow widths, preserve
|
|
51
|
+
the full title and persistent actions by stacking the trailing actions below
|
|
52
|
+
the title rather than clipping either region.
|
|
53
|
+
9. For team administration and account settings, read
|
|
40
54
|
`docs/patterns/application_foundation.md`. Put Settings after an
|
|
41
|
-
`AppNavigation` spacer and use `SettingsLayout` with plain `
|
|
55
|
+
`AppNavigation` spacer and use `SettingsLayout` with plain `SettingsSection`
|
|
42
56
|
regions instead of a stack of Cards.
|
|
43
57
|
|
|
44
58
|
## Preserve the boundary
|
|
@@ -53,10 +67,13 @@ If the gem is not installed, say that the skill requires Nitro Kit and follow th
|
|
|
53
67
|
- Never downgrade a specialized control to a generic Nitro control for visual
|
|
54
68
|
consistency, and never retain copied Nitro Kit 1.x source as the fallback.
|
|
55
69
|
- Do not introduce `nk_*` helpers, a general ERB bridge, or generated variant helpers.
|
|
56
|
-
- Do not pass `class:` or `style:`. Prefer component options, composition, or theme tokens. Use `desperately_need_a_class:` only for a
|
|
70
|
+
- Pass native attributes through each component method's documented `html:`, `aria:`, or `data:` boundary; for example, use `table.tr(html: { id: dom_id(record) })`, not `table.tr(id: ...)`. Do not pass `class:` or `style:`. Prefer component options, composition, wrappers, or theme tokens. Use `desperately_need_a_class:` only for a named external integration boundary that requires a class hook; it accepts Rails-style strings, symbols, nested arrays, and conditional hashes without manual joining.
|
|
71
|
+
- Give every icon-only Button, Dropdown trigger, and Sheet trigger an explicit `label:` or ARIA label. For custom `form.field` blocks, render an explicit field label instead of relying on an unused implicit model translation.
|
|
57
72
|
- Do not add application-specific behavior to Nitro-owned Stimulus controllers.
|
|
58
73
|
- Do not recreate a Nitro component with raw HTML unless the installed catalog cannot express the semantics.
|
|
59
74
|
|
|
60
75
|
## Verify
|
|
61
76
|
|
|
62
77
|
Run the smallest relevant application tests. For component rendering, assert semantic elements and owned `data-nk` or slot attributes rather than private implementation helpers. Exercise invalid and empty states when the UI accepts user input or collections.
|
|
78
|
+
|
|
79
|
+
For a migration, Doctor is an inventory, not visual proof. Run representative form and component rendering with `ActiveModel::Translation.raise_on_missing_translations` enabled when the application uses strict i18n. Compare the same representative flows in a browser at wide and narrow widths, exercise keyboard focus, and inspect computed styles for missing application classes, stacked Button content, broken compound corners, double focus rings, clipping, and theme drift. Re-audit rendered native buttons, Rails button helpers, and application-owned button classes before declaring the conversion complete. Search the whole application for `desperately_need_a_class:` and review every result, aiming for zero. Move layout and visual treatment to application-owned wrappers, remove generic class forwarding, accept incidental Nitro defaults, and keep unmatched product UI application-owned; retain only documented external-integration hooks.
|