nitro_kit 2.0.0.alpha.3 → 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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +179 -0
  3. data/README.md +22 -26
  4. data/STYLE_GUIDE.md +10 -0
  5. data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +3 -3
  6. data/app/assets/stylesheets/nitro_kit.css +1220 -572
  7. data/app/components/nitro_kit/accordion.rb +1 -19
  8. data/app/components/nitro_kit/alert.rb +1 -11
  9. data/app/components/nitro_kit/app_navigation.rb +44 -9
  10. data/app/components/nitro_kit/appearance_picker.rb +10 -1
  11. data/app/components/nitro_kit/badge.rb +13 -4
  12. data/app/components/nitro_kit/combobox.rb +1 -17
  13. data/app/components/nitro_kit/command_palette.rb +1 -0
  14. data/app/components/nitro_kit/dropzone.rb +1 -1
  15. data/app/components/nitro_kit/form_builder.rb +1 -1
  16. data/app/components/nitro_kit/select.rb +1 -14
  17. data/app/components/nitro_kit/tabs.rb +1 -0
  18. data/app/javascript/controllers/nk/command_palette_controller.js +4 -0
  19. data/app/javascript/controllers/nk/tabs_controller.js +13 -1
  20. data/docs/agent_guide.md +63 -138
  21. data/docs/agent_native_spec.md +72 -400
  22. data/docs/browser_support.md +77 -188
  23. data/docs/component_contracts.md +56 -36
  24. data/docs/customization.md +675 -89
  25. data/docs/hotwire.md +61 -99
  26. data/docs/initialization_prompt.md +27 -57
  27. data/docs/migration_1_to_2.md +34 -18
  28. data/docs/patterns/application_foundation.md +45 -121
  29. data/docs/patterns/crud_resource.md +44 -129
  30. data/docs/patterns/destructive_action.md +45 -102
  31. data/docs/patterns/flash_and_toast.md +18 -36
  32. data/docs/patterns/inline_edit.md +21 -44
  33. data/docs/patterns/queryable_collection.md +65 -161
  34. data/docs/patterns/resource_form.md +33 -81
  35. data/docs/rails_conventions.md +38 -78
  36. data/docs/rails_integration.md +96 -474
  37. data/lib/nitro_kit/installation.rb +11 -3
  38. data/lib/nitro_kit/migration_inventory.rb +15 -8
  39. data/lib/nitro_kit/version.rb +1 -1
  40. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +7 -0
  41. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +15 -8
  42. data/src/stylesheets/nitro_kit/components/accordion.css +4 -4
  43. data/src/stylesheets/nitro_kit/components/alert.css +20 -29
  44. data/src/stylesheets/nitro_kit/components/app_navigation.css +74 -2
  45. data/src/stylesheets/nitro_kit/components/app_shell.css +3 -3
  46. data/src/stylesheets/nitro_kit/components/appearance_picker.css +20 -8
  47. data/src/stylesheets/nitro_kit/components/auth_shell.css +2 -2
  48. data/src/stylesheets/nitro_kit/components/avatar.css +12 -8
  49. data/src/stylesheets/nitro_kit/components/avatar_stack.css +12 -10
  50. data/src/stylesheets/nitro_kit/components/badge.css +13 -9
  51. data/src/stylesheets/nitro_kit/components/button.css +42 -25
  52. data/src/stylesheets/nitro_kit/components/card.css +2 -2
  53. data/src/stylesheets/nitro_kit/components/checkbox.css +29 -46
  54. data/src/stylesheets/nitro_kit/components/checkbox_group.css +3 -3
  55. data/src/stylesheets/nitro_kit/components/combobox.css +6 -6
  56. data/src/stylesheets/nitro_kit/components/command_palette.css +62 -4
  57. data/src/stylesheets/nitro_kit/components/container.css +6 -6
  58. data/src/stylesheets/nitro_kit/components/control_group.css +9 -9
  59. data/src/stylesheets/nitro_kit/components/danger_zone.css +6 -6
  60. data/src/stylesheets/nitro_kit/components/data_section.css +4 -4
  61. data/src/stylesheets/nitro_kit/components/details_table.css +7 -5
  62. data/src/stylesheets/nitro_kit/components/dialog.css +3 -3
  63. data/src/stylesheets/nitro_kit/components/dropdown.css +7 -7
  64. data/src/stylesheets/nitro_kit/components/dropzone.css +7 -7
  65. data/src/stylesheets/nitro_kit/components/empty_state.css +4 -4
  66. data/src/stylesheets/nitro_kit/components/field.css +2 -2
  67. data/src/stylesheets/nitro_kit/components/fieldset.css +2 -2
  68. data/src/stylesheets/nitro_kit/components/flex.css +1 -1
  69. data/src/stylesheets/nitro_kit/components/grid.css +1 -1
  70. data/src/stylesheets/nitro_kit/components/icon.css +5 -5
  71. data/src/stylesheets/nitro_kit/components/input.css +33 -6
  72. data/src/stylesheets/nitro_kit/components/page_header.css +4 -4
  73. data/src/stylesheets/nitro_kit/components/pagination.css +3 -3
  74. data/src/stylesheets/nitro_kit/components/pagination_bar.css +2 -2
  75. data/src/stylesheets/nitro_kit/components/palette.css +140 -72
  76. data/src/stylesheets/nitro_kit/components/progressive_image.css +1 -1
  77. data/src/stylesheets/nitro_kit/components/radio_button.css +28 -33
  78. data/src/stylesheets/nitro_kit/components/radio_button_group.css +6 -6
  79. data/src/stylesheets/nitro_kit/components/rich_text_area.css +2 -2
  80. data/src/stylesheets/nitro_kit/components/select.css +24 -8
  81. data/src/stylesheets/nitro_kit/components/settings_layout.css +5 -5
  82. data/src/stylesheets/nitro_kit/components/settings_section.css +2 -2
  83. data/src/stylesheets/nitro_kit/components/sheet.css +10 -10
  84. data/src/stylesheets/nitro_kit/components/stat_grid.css +1 -1
  85. data/src/stylesheets/nitro_kit/components/switch.css +34 -40
  86. data/src/stylesheets/nitro_kit/components/table.css +8 -4
  87. data/src/stylesheets/nitro_kit/components/tabs.css +5 -1
  88. data/src/stylesheets/nitro_kit/components/textarea.css +20 -5
  89. data/src/stylesheets/nitro_kit/components/toast.css +11 -58
  90. data/src/stylesheets/nitro_kit/components/toolbar.css +2 -2
  91. data/src/stylesheets/nitro_kit/components/tooltip.css +4 -0
  92. data/src/stylesheets/nitro_kit/components/typeset.css +4 -4
  93. data/src/stylesheets/nitro_kit/reset.css +6 -6
  94. data/src/stylesheets/nitro_kit/tokens.css +497 -74
  95. metadata +1 -2
  96. data/docs/new_app_strategy.md +0 -22
@@ -1,134 +1,47 @@
1
- # Rails and Hotwire integration
1
+ # Rails integration
2
2
 
3
- Nitro Kit 2.0 uses Rails where Rails owns important application semantics: model-backed forms, routes, DOM IDs, and Hotwire. The supported view layer remains direct Phlex. There is no `nk_form_with`, `nk_form_for`, or general ERB component bridge.
3
+ **Audience:** Application developers and coding agents installing Nitro Kit or
4
+ connecting it to Rails forms, assets, Stimulus, and Hotwire.
4
5
 
5
- ## Installation and assets
6
+ Rails owns records, routes, DOM IDs, forms, authorization, and responses.
7
+ Nitro Kit owns Phlex components, presentation, and focused browser behavior.
8
+ There are no `nk_form_with` helpers or general ERB component bridge.
6
9
 
7
- Add the 2.0 prerelease to the application's Gemfile:
10
+ ## Install
11
+
12
+ Pin the current prerelease:
8
13
 
9
14
  ```ruby
10
- gem "nitro_kit", "2.0.0.alpha.3"
15
+ gem "nitro_kit", "2.0.0.alpha.4"
11
16
  ```
12
17
 
13
- Bundler records the exact released version in `Gemfile.lock`; commit `Gemfile`
14
- and `Gemfile.lock` together. Before upgrading, review the changelog, run
15
- `bundle update nitro_kit`, rerun the generator and doctor, and run the
16
- application's focused and full tests before committing the updated lockfile.
17
- Production applications should use the released gem and a committed lockfile
18
- rather than a moving Git branch.
18
+ Use the released gem and commit `Gemfile` with `Gemfile.lock`. Before upgrading,
19
+ review the changelog, run `bundle update nitro_kit`, rerun the installer, and
20
+ test the application before committing the lockfile.
19
21
 
20
22
  ```sh
21
23
  bundle install
22
24
  bin/rails generate nitro_kit:install
25
+ bin/rails nitro_kit:doctor
23
26
  ```
24
27
 
25
- Load the static stylesheet before application styles through the Rails asset pipeline:
28
+ The installer adds project-local agent guidance and completes conventional
29
+ layouts when the insertion points are unambiguous. It leaves dynamic or custom
30
+ layouts unchanged and reports manual work. It never copies component source.
31
+
32
+ Load Nitro Kit before application styles:
26
33
 
27
34
  ```erb
28
35
  <%= stylesheet_link_tag "nitro_kit", "application", "data-turbo-track": "reload" %>
29
36
  ```
30
37
 
31
- Nitro Kit does not require Tailwind. A Tailwind CSS v4 application loads `nitro_kit-tailwind-v4`, Nitro Kit, compiled Tailwind, and application styles in that order. The adapter establishes cascade order and maps Nitro theme tokens to common Tailwind theme variables.
32
-
33
- Keep application token overrides after Nitro Kit. The [customization guide](customization.md) documents the exact load order, every supported token, scoped and appearance-specific overrides, the theme customizer, and the optional Tailwind adapter.
34
-
35
- Raised default Buttons have their own public background, hover, foreground, and border tokens. Override `--nk-button-default-*` rather than changing `--nk-color-surface` when form controls, cards, dialogs, and menus should retain their existing surfaces.
36
-
37
- The install generator writes project-owned agent guidance — a managed Nitro Kit
38
- 2 block in `AGENTS.md` and thin skill routers under `.agents/skills` and
39
- `.claude/skills` — and additively completes conventional ERB and Phlex layouts.
40
- It preserves existing bootstrap calls, stylesheet expressions, and their
41
- options. It inserts only missing entries where order is unambiguous, and adds
42
- `application` only when that asset exists or the layout already names it.
43
- Dynamic, conditional, or custom layouts remain unchanged and doctor reports a
44
- manual repair. The resulting order keeps optional third-party base styles such
45
- as Lexxy first, then the optional Tailwind adapter, Nitro Kit, compiled
46
- Tailwind, and application styles. A Lexxy application therefore has this exact
47
- three-entry baseline:
48
-
49
- ```ruby
50
- stylesheet_link_tag("lexxy", "nitro_kit", "application", data: { turbo_track: "reload" })
51
- ```
52
-
53
- There is no component source-copy step. Re-run the generator after upgrading
54
- the gem, then run `bin/rails nitro_kit:doctor`. Doctor fails for a missing,
55
- duplicate, or misordered bootstrap or stylesheet entry and reports the exact
56
- repair. Use `--format=json` for structured automation output.
57
-
58
- Install a focused upgrade acceptance flow separately:
59
-
60
- ```sh
61
- bin/rails generate nitro_kit:upgrade_smoke_tests
62
- bin/rails test test/integration/nitro_kit_upgrade_smoke_test.rb
63
- bin/rails test test/system/nitro_kit_upgrade_smoke_test.rb
64
- ```
65
-
66
- The generator creates each test only when its path is absent and its host
67
- prerequisite exists: `test/test_helper.rb` for integration coverage and
68
- `test/application_system_test_case.rb` for browser coverage. It prints an
69
- actionable message for unsupported pieces and never overwrites
70
- application-owned coverage. Using the currently bundled gem, the tests prepend
71
- a route only for each test, then restore host routing. Catch-all routes remain
72
- compatible, while an exact GET or PATCH collision is rejected. The
73
- endpoint inherits the host `ApplicationController` and renders through the
74
- application layout, so Nitro CSS, the appearance bootstrap, host JavaScript and
75
- CSP handling remain in the exercised path. Coverage includes browser-submitted
76
- Turbo validation and mutation, 303 redirect, layout-owned flash-to-Toast
77
- feedback, stable Turbo Frame identity, Dialog and Sheet behavior, and
78
- post-mutation Phlex rendering. No production route or application source is
79
- added.
80
-
81
- The endpoint intentionally keeps every `ApplicationController` callback. The
82
- generated classes expose a setup hook for authentication and account context;
83
- replace these example helper names with the host application's real test API:
84
-
85
- ```ruby
86
- class NitroKitUpgradeSmokeTest < NitroKit::UpgradeSmokeTest
87
- private
88
- def prepare_nitro_kit_upgrade_smoke_test
89
- sign_in users(:owner)
90
- select_account accounts(:primary)
91
- end
92
- end
93
-
94
- class NitroKitUpgradeSmokeSystemTest < ApplicationSystemTestCase
95
- include NitroKit::UpgradeSmokeSystemTests
38
+ For third-party base CSS, Tailwind, appearance setup, and token overrides, use
39
+ the canonical [stylesheet order](customization.md#stylesheet-order).
96
40
 
97
- private
98
- def prepare_nitro_kit_upgrade_smoke_test
99
- sign_in_as users(:owner)
100
- select_account accounts(:primary)
101
- end
102
- end
103
- ```
41
+ ## Stimulus
104
42
 
105
- The hook runs before the inherited requests or browser visits. These generated
106
- files are application-owned extension points: use the same session, sign-in,
107
- and account-selection path as other host tests. Do not skip callbacks or alter
108
- the gem controller, because that would bypass the integration under test.
109
-
110
- ## Stimulus and importmap
111
-
112
- Enhanced components use gem-owned Stimulus controllers, including `nk--app-shell`, `nk--appearance`, `nk--avatar`, `nk--checkable`, `nk--combobox`, `nk--dialog`, `nk--dropdown`, `nk--dropzone`, `nk--progressive-image`, `nk--tabs`, `nk--toast`, and `nk--tooltip`.
113
-
114
- Accordion disclosure is controller-free. Its shared `name` is the native
115
- single-group authority; browsers without named-details grouping retain full
116
- disclosure but have the documented reduced, independently-open single-mode
117
- baseline. Dialog and Sheet prefer declarative
118
- `command`/`commandfor`, while `nk--dialog` owns backdrop and cancel policy. The
119
- controller checks each invoker's reflected relationship and uses
120
- `HTMLDialogElement.showModal()` or `close()` only when the native command will
121
- not run. CommandPalette uses the same bridge instead of replacing the native
122
- path. Without JavaScript, these controls still require native Invoker Commands.
123
- Server-rendered content inside a closed overlay is therefore not itself a
124
- JavaScript-free fallback; critical content needs an ordinary server review
125
- route. The canonical classification matrix is in
126
- [`browser_support.md`](browser_support.md).
127
- Dropdown uses native Popover as its source of truth and adds menu keyboard focus
128
- and positioning; Tooltip uses CSS for hover/focus and JavaScript only for Escape
129
- dismissal. See the [browser support policy](browser_support.md).
130
-
131
- When `importmap-rails` is present, the engine adds its importmap and asset paths automatically. The application must still install Stimulus and provide the normal controller loader:
43
+ Importmap applications receive Nitro Kit's pins from the engine. The host still
44
+ owns Stimulus and its normal loader:
132
45
 
133
46
  ```js
134
47
  import { application } from "controllers/application";
@@ -137,406 +50,115 @@ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
137
50
  eagerLoadControllersFrom("controllers", application);
138
51
  ```
139
52
 
140
- Nitro Kit packages no third-party JavaScript. Accordion disclosure, date inputs,
141
- and Switch use native browser behavior. Dialog and Sheet mount their packaged
142
- controller for dismissal policy, Turbo cache cleanup, and the feature-detected
143
- Invoker Commands bridge.
53
+ Do not copy `nk--*` controllers into the application. Nitro Kit currently has
54
+ no JavaScript-package entrypoint for automatic bundler registration. Without
55
+ the packaged controllers, Ruby and CSS remain available with the reduced
56
+ baselines in [Browser support](browser_support.md).
144
57
 
145
- The engine deliberately boots when importmap is absent. In that configuration, Ruby and CSS remain available, but automatic JavaScript registration does not: a bundler-based application must expose and register the controller modules itself. Nitro Kit 2.0 does not ship a JavaScript-package entrypoint.
58
+ ## Appearance and CSP
146
59
 
147
- ## Appearance and content security policy
148
-
149
- Render the non-visual bootstrap in the document `head` before every stylesheet link. Its fixed script body restores the validated `light`, `dark`, or `system` preference before CSS-visible paint. The optional picker can appear zero, one, or many times; every picker reflects the same document preference.
60
+ Render `AppearanceBootstrap` before stylesheets. Render zero or more pickers in
61
+ the body:
150
62
 
151
63
  ```ruby
152
- class ApplicationLayout < Phlex::HTML
153
- include Phlex::Rails::Layout
154
- include Phlex::Rails::Helpers::ContentSecurityPolicyNonce
155
-
156
- def view_template
157
- doctype
158
- html(lang: "en") do
159
- head do
160
- render NitroKit::AppearanceBootstrap.new(
161
- default: :system,
162
- nonce: content_security_policy_nonce
163
- )
164
- stylesheet_link_tag("nitro_kit", data: { turbo_track: "reload" })
165
- end
64
+ head do
65
+ render NitroKit::AppearanceBootstrap.new(
66
+ default: :system,
67
+ nonce: content_security_policy_nonce
68
+ )
69
+ stylesheet_link_tag("nitro_kit", data: { turbo_track: "reload" })
70
+ stylesheet_link_tag("application", data: { turbo_track: "reload" })
71
+ end
166
72
 
167
- body do
168
- render NitroKit::AppearancePicker.new(
169
- id: "application-appearance",
170
- label: "Appearance"
171
- )
172
- yield
173
- end
174
- end
175
- end
73
+ body do
74
+ render NitroKit::AppearancePicker.new(
75
+ id: "application-appearance",
76
+ label: "Appearance"
77
+ )
78
+ yield
176
79
  end
177
80
  ```
178
81
 
179
- The runtime stores the preference under `nitro-kit-appearance`. It writes `data-theme-preference="light|dark|system"` and the resolved `data-theme="light|dark"` on the document root. System mode follows live operating-system changes; explicit choices do not. Storage denial falls back to `default:` and does not prevent in-document changes. Nitro does not synchronize this browser preference to an application user record.
180
-
181
- For nonce-based policies, pass Rails' `content_security_policy_nonce` as above and include the generated nonce in the application's `script-src` policy. For hash-based policies, allow Nitro's exact fixed script body with:
82
+ For a nonce policy, pass Rails' `content_security_policy_nonce`. For a hash
83
+ policy, allow the current fixed body:
182
84
 
183
85
  ```text
184
86
  script-src 'self' 'sha256-Vcime4euWSeYtHSfjYjqz/XhRyzMcLpn6Ip2LlaHleY='
185
87
  ```
186
88
 
187
- The same value is available as `NitroKit::AppearanceBootstrap::CSP_HASH`. The hash covers only the fixed inline body; `default:` lives in a data attribute and a nonce lives on the script element, so neither changes it. Recheck the constant when upgrading Nitro Kit because an intentional runtime change produces a new hash.
188
-
189
- If the bootstrap is blocked or omitted, Nitro's token CSS follows `prefers-color-scheme`. An explicit `[data-theme="light"]` or `[data-theme="dark"]` on a document or containing theme root overrides that fallback. `data-theme` always names the resolved appearance; system preference is recorded separately in `data-theme-preference`. See [Customizing Nitro Kit](customization.md#global-overrides) for matching light, dark, system-fallback, and scoped CSS recipes.
190
-
191
- ## Application shells
192
-
193
- `AppShell` composes directly in Phlex and keeps Rails route policy in the application. It requires one navigation and one main region; brand and topbar regions are optional:
194
-
195
- ```ruby
196
- render NitroKit::AppShell.new(id: "workspace", layout: :sidebar) do |shell|
197
- shell.brand { strong { "Northstar" } }
198
-
199
- shell.navigation do
200
- render NitroKit::AppNavigation.new(label: "Primary navigation") do |navigation|
201
- navigation.body do
202
- navigation.item("Overview", href: root_path, icon: :house, current: true)
203
- navigation.item("Projects", href: projects_path, icon: :folder)
204
- navigation.spacer
205
- navigation.item("Settings", href: settings_path, icon: :settings)
206
- end
207
- end
208
- end
209
-
210
- shell.topbar do
211
- render NitroKit::Button.new("New project", href: new_project_path, variant: :primary)
212
- end
213
-
214
- shell.main { render Workspace::Dashboard.new }
215
- end
216
- ```
217
-
218
- The same declarations work with `layout: :topbar` and `layout: :hybrid`. Nitro owns responsive disclosure and focus behavior; the application owns destinations, authorization, current-route selection, and page content. The [customization guide](customization.md#application-shells) covers shell tokens, composition boundaries, and the three complete gallery applications.
219
-
220
- Large destination sets may add one command palette to the shell:
221
-
222
- ```ruby
223
- render NitroKit::CommandPalette.new(id: "workspace-search", label: "Search workspace…") do |palette|
224
- palette.destination("Overview", href: root_path, description: "Workspace")
225
- palette.destination("Projects", href: projects_path, description: "Workspace")
226
- palette.destination("Settings", href: settings_path, description: "Account")
227
- end
228
- ```
229
-
230
- The application remains responsible for authorization and must render only destinations the current user may visit. Stimulus adds filtering and the Command-K or Control-K shortcut. Without JavaScript, the trigger and destination links are available only where declarative dialog commands are supported. Use `shortcut: false` for any additional palette on the same document so only one component owns the global shortcut.
231
-
232
- ### Server-rendered command palette results
233
-
234
- For a large or dynamic destination set, pass a GET endpoint through `search_url:`. Keep a useful authorized set in the declaration block: those links are the immediate first render and the no-JavaScript baseline in browsers with declarative dialog commands.
235
-
236
- ```ruby
237
- render NitroKit::CommandPalette.new(
238
- id: "workspace-search",
239
- label: "Search workspace…",
240
- search_url: command_palette_results_path
241
- ) do |palette|
242
- current_user.recent_destinations.each do |destination|
243
- palette.destination(destination.name, href: destination.path, description: destination.section)
244
- end
245
- end
246
- ```
247
-
248
- Enhancement turns the search region into a debounced GET form. Its input is named `query` and targets the palette's owned Turbo Frame. Use an ordinary REST collection endpoint:
249
-
250
- ```ruby
251
- # config/routes.rb
252
- resources :command_palette_results, only: :index
253
-
254
- # app/controllers/command_palette_results_controller.rb
255
- class CommandPaletteResultsController < ApplicationController
256
- def index
257
- @destinations = Current.user.destinations.search(params[:query])
258
- end
259
- end
260
- ```
261
-
262
- Return the matching frame with `CommandPalette::Results`. Its `id:` must exactly match the parent palette's stable `id:`. An empty result block is valid; Nitro shows the translated empty state and updates the live result count.
263
-
264
- ```erb
265
- <%# app/views/command_palette_results/index.html.erb %>
266
- <%= render NitroKit::CommandPalette::Results.new(id: "workspace-search") do |results| %>
267
- <% @destinations.each do |destination| %>
268
- <% results.destination(
269
- destination.name,
270
- href: destination.path,
271
- description: destination.section
272
- ) %>
273
- <% end %>
274
- <% end %>
275
- ```
276
-
277
- The response is HTML, not JSON and not a Turbo Stream. Turbo replaces only the results frame; destination links target the full page. Scope and authorize every query on the server rather than sending hidden destinations to the browser and filtering them there.
89
+ The value is also `NitroKit::AppearanceBootstrap::CSP_HASH`; recheck it after
90
+ upgrades. The runtime stores the preference under `nitro-kit-appearance`, sets
91
+ resolved `data-theme="light|dark"`, and keeps the selected preference in
92
+ `data-theme-preference`. Nitro does not synchronize it to a user record.
278
93
 
279
94
  ## Model-backed forms
280
95
 
281
- Include the Rails helpers a Phlex component actually uses, then select `NitroKit::FormBuilder` explicitly:
96
+ Select the builder explicitly and group visible fields and actions:
282
97
 
283
98
  ```ruby
284
99
  class RegistrationForm < Phlex::HTML
285
- include Phlex::Rails::Helpers::DOMID
286
100
  include Phlex::Rails::Helpers::FormWith
287
- include Phlex::Rails::Helpers::Routes
288
- include Phlex::Rails::Helpers::TurboFrameTag
289
101
 
290
102
  def initialize(registration)
291
103
  @registration = registration
292
104
  end
293
105
 
294
106
  def view_template
295
- turbo_frame_tag(dom_id(@registration, :form)) do
296
- form_with(
297
- model: @registration,
298
- url: registration_path,
299
- builder: NitroKit::FormBuilder,
300
- id: dom_id(@registration, :details)
301
- ) do |form|
302
- form.hidden_field(:source)
303
- form.group do
304
- form.field(:email, as: :email, required: true)
305
- form.field(
306
- :role,
307
- as: :select,
308
- options: [["Developer", "developer"], ["Designer", "designer"]],
309
- prompt: "Choose a role",
310
- required: true
311
- )
312
- form.field(:terms, as: :checkbox, label: "I accept the terms")
313
- form.field(:attachment, as: :file, accept: "text/plain")
314
- form.submit("Register", data: { turbo_submits_with: "Registering…" })
315
- end
316
- end
317
- end
318
- end
319
- end
320
- ```
321
-
322
- `form.field` is the canonical Nitro API. It preserves Rails-generated names, IDs, model values, values-before-type-cast, and errors while rendering the Nitro `Field` and control contracts. A file field marks the enclosing form as `multipart/form-data`. Checkbox fields emit the unchecked hidden value before the checkbox.
323
-
324
- `form.group` supplies the default vertical rhythm between a standalone form's
325
- visible fields, submit control, and related links. Keep hidden fields outside
326
- the group when convenient; they do not participate in layout. Use more than
327
- one group only when the form has genuinely distinct sections.
328
-
329
- The builder also supports Rails-shaped control methods such as `text_field`, `email_field`, `file_field`, `check_box`, `hidden_field`, and `select`. Their ordinary native options belong to the control:
330
-
331
- ```ruby
332
- form.email_field(
333
- :email,
334
- maxlength: 120,
335
- data: { action: "input->signup#validate" },
336
- aria: { describedby: "email-help" }
337
- )
338
-
339
- form.select(:role, nil) do
340
- option(value: "developer") { "Developer" }
341
- option(value: "designer") { "Designer" }
342
- end
343
- ```
344
-
345
- Captured select blocks stay inside the native `<select>`. Explicit `selected:` values, including arrays for multiple selects, override the model value. `prompt: true` uses Rails' translated “Please select” prompt.
346
-
347
- `hidden_field` intentionally renders a standalone hidden Nitro input rather than a visible Field wrapper. `class` and `style` remain rejected. Every builder method uses one boundary: `html:`, `aria:`, and `data:` decorate the control, `control_html:`, `control_aria:`, and `control_data:` are their explicit long forms, and giving the same key through both raises. Decorate the Field wrapper with `wrapper_html:`, `wrapper_aria:`, and `wrapper_data:`.
348
-
349
- ### Rich text with Lexxy
350
-
351
- Lexxy is Nitro Kit's preferred Action Text editor. Once the application has
352
- installed Action Text and Lexxy, use the same builder API as every other field:
353
-
354
- ```ruby
355
- form.field(:brief, as: :rich_text, placeholder: "Describe the project")
356
- ```
357
-
358
- Nitro wraps the editor in the ordinary Field contract, so labels, descriptions,
359
- validation errors, layout, and theme tokens remain consistent. Lexxy continues
360
- to own its hidden input, attachment flow, editor behavior, prompts, and native
361
- options; pass editor-specific attributes through `control_html:`. Nitro Kit does
362
- not bundle or fork Lexxy's JavaScript.
363
-
364
- The complete builder surface includes:
365
-
366
- - `field`, `fieldset`, and `group`.
367
- - `dropzone` for native file selection with optional Active Storage direct uploads.
368
- - `select`, `radio_button`, `check_box`/`checkbox`, and `hidden_field`.
369
- - `submit` and `button`.
370
- - Rails-shaped color, date, datetime, email, file, month, number, password, phone/telephone, range, rich text, search, text, textarea, time, URL, and week fields.
371
-
372
- `month_field` and `week_field` emit native `month` and `week` input types as
373
- progressive enhancement. Some supported desktop browsers expose ordinary text
374
- entry, and older iOS Safari releases lack week selection, so do not rely on a
375
- picker, browser normalization, or `min`, `max`, and `step`. Validate `YYYY-MM`
376
- and `YYYY-Www` plus domain range and increment rules on the model. If the user
377
- must choose from an exact bounded set, render `form.field(..., as: :select,
378
- options: ...)` with application-owned options. Nitro does not ship a generic
379
- datepicker for this case.
380
-
381
- `submit` renders a primary submit Button named `commit`, and `button` defaults
382
- to `type: :submit`. The Rails helpers Nitro does not style — `label`,
383
- `collection_select`, `grouped_collection_select`, `collection_radio_buttons`,
384
- `collection_check_boxes`, `date_select`, and `time_zone_select` — raise and name
385
- their `form.field(as:)` equivalent instead of leaking unstyled markup.
386
-
387
- ### Mutation buttons and joined controls
388
-
389
- Navigation uses `Button.new(..., href:)`. A non-GET action uses `ButtonTo`,
390
- which renders one Rails method form and one submit Button:
391
-
392
- ```ruby
393
- render NitroKit::ButtonTo.new(
394
- "Revoke token",
395
- href: token_path(token),
396
- method: :delete,
397
- variant: :destructive,
398
- data: { turbo_confirm: "Revoke this token?" }
399
- )
400
- ```
401
-
402
- Root `html:`, `aria:`, and `data:` address the form. `button_html:`,
403
- `button_aria:`, and `button_data:` address the nested focusable Button when a
404
- composition such as Tooltip must attach attributes there. The form is
405
- layout-transparent, so ButtonTo participates in Flex, Grid, and action rows as
406
- its Button.
407
-
408
- Use `ControlGroup` when adjacent native controls intentionally share borders:
409
-
410
- ```ruby
411
- render NitroKit::ControlGroup.new(label: "Copy webhook URL") do
412
- render NitroKit::Input.new(value: webhook_url, readonly: true)
413
- render NitroKit::Button.new("Copy", type: :button, icon: :copy)
414
- end
415
- ```
416
-
417
- Direct Input, Select, and Button children keep their own values and behavior.
418
- `group.addon("https://")` adds a textual prefix, suffix, or unit. Do not use a
419
- ControlGroup merely to reduce ordinary form spacing; FieldGroup owns vertical
420
- form rhythm.
421
-
422
- ### File drops and direct uploads
423
-
424
- `form.dropzone` derives the native input ID and Rails parameter name, marks the form as multipart, and accepts the same explicit upload contract as `NitroKit::Dropzone`:
425
-
426
- ```ruby
427
- form.dropzone(
428
- :attachments,
429
- label: "Upload evidence",
430
- description: "Up to three PDF files, each no larger than 5 MB.",
431
- multiple: true,
432
- accept: "application/pdf",
433
- max_files: 3,
434
- max_bytes: 5 * 1024 * 1024,
435
- required: true
436
- )
437
- ```
438
-
439
- The labelled `<input type="file">` remains the source of truth. Without JavaScript it submits ordinary uploaded files. With the controller connected, selection and dropping add removable previews, enforce the declared count, byte, and type constraints, and announce upload and error state. Set `direct_upload: false` to keep the selected `File` objects on that input for the normal multipart request.
440
-
441
- The default `direct_upload: true` uses Rails' public `DirectUpload` client. Nitro Kit pins `@rails/activestorage` for importmap applications; bundler-based applications must make that module available alongside the Nitro controller. The host application must install Active Storage's tables, configure a service, and expose the standard `rails_direct_uploads_path` route. Successful uploads submit signed blob IDs under the same Rails parameter name. Removing or replacing a file removes its signed ID, and the form's submit controls remain unavailable while uploads are active.
442
-
443
- ## Validation responses
444
-
445
- Build the model from submitted parameters and render the same Phlex form with status 422 when it is invalid. `NitroKit::FormBuilder` reads the model's real `ActiveModel::Errors`; Field connects help and error IDs through `aria-describedby` and sets `aria-invalid`.
446
-
447
- ```ruby
448
- def create
449
- @registration = Registration.new(registration_params)
450
- @registration.valid? ? render_success : render_errors
451
- end
452
-
453
- private
454
- def render_errors
455
- respond_to do |format|
456
- format.turbo_stream do
457
- render RegistrationStream.new(@registration), status: :unprocessable_entity
458
- end
459
- format.html do
460
- render RegistrationForm.new(@registration), status: :unprocessable_entity
107
+ form_with(model: @registration, builder: NitroKit::FormBuilder) do |form|
108
+ form.hidden_field(:source)
109
+ form.group do
110
+ form.field(:email, as: :email, required: true)
111
+ form.field(
112
+ :role,
113
+ as: :select,
114
+ options: [["Developer", "developer"], ["Designer", "designer"]]
115
+ )
116
+ form.field(:attachment, as: :file, accept: "text/plain")
117
+ form.submit("Register")
461
118
  end
462
119
  end
463
120
  end
464
- ```
465
-
466
- Keep the HTML branch. It is the progressive fallback when Turbo is unavailable.
467
-
468
- ## Turbo Frames and Streams
469
-
470
- Use Rails' DOM helper for stable frame targets. A form inside a frame submits to that frame by default; use `data: { turbo_frame: "_top" }` only for navigation that should leave it.
471
-
472
- Turbo Stream responses can also be Phlex components:
473
-
474
- ```ruby
475
- class RegistrationStream < Phlex::HTML
476
- include Phlex::Rails::Helpers::DOMID
477
- include Phlex::Rails::Helpers::TurboStream
478
-
479
- def initialize(registration)
480
- @registration = registration
481
- end
482
-
483
- def view_template
484
- turbo_stream.replace(dom_id(@registration, :form)) do
485
- render RegistrationForm.new(@registration)
486
- end
487
- end
488
121
  end
489
122
  ```
490
123
 
491
- Deliver the submitting user's stream over the HTTP response. A successful non-Turbo POST should redirect with `303 See Other`; an invalid HTML or Turbo submission should return 422. Reserve Action Cable broadcasts for updates that must reach other sessions.
492
-
493
- The dummy application's `RailsIntegration::RegistrationForm`,
494
- `RegistrationStream`, `RegistrationSuccess`, and request/system tests are an
495
- executable reference implementation of this contract. The form submits a
496
- note. A 422 response replaces `form_registration` with the same invalid Phlex
497
- form and preserves that submitted note; a successful stream replaces the same
498
- frame with `RegistrationSuccess` and renders the submitted email and note.
499
- The tests scope those assertions to
500
- `turbo-stream > template > turbo-frame#form_registration` and, in the browser,
501
- to the live `turbo-frame#form_registration`. This matching-ID boundary is
502
- intentional: a page-level text assertion can pass while a frame response is
503
- missing its target and Turbo renders nothing.
504
-
505
- ## Pagy pagination
506
-
507
- Keep the collection query in the controller and pass Pagy's result directly to Pagination:
124
+ `form.field` preserves Rails-generated names, IDs, model values, errors, file
125
+ multipart behavior, and checkbox hidden values. `form.group` owns vertical
126
+ rhythm. Hidden fields may remain outside it.
508
127
 
509
- ```ruby
510
- class ProjectsController < ApplicationController
511
- def index
512
- @pagy, @projects = pagy(:offset, Project.order(updated_at: :desc))
513
- end
514
- end
515
- ```
128
+ Use `html:`, `aria:`, and `data:` for control attributes. Use
129
+ `wrapper_html:`, `wrapper_aria:`, and `wrapper_data:` for the Field wrapper.
130
+ The [component contracts](component_contracts.md) list supported field types
131
+ and exact option boundaries.
516
132
 
517
- ```ruby
518
- Pagination(pagy: @pagy)
519
- ```
520
-
521
- Pagination reads Pagy's previous page, visible series, gaps, current page, next page, and page URLs. Pagy remains optional: applications that do not bundle it keep using the manual declaration API. Modern Pagy objects own URL generation through their request context. For an older Pagy release or a custom compatible object, supply the URL boundary explicitly:
133
+ ### Optional integrations
522
134
 
523
- ```ruby
524
- Pagination(
525
- pagy: @pagy,
526
- page_url: ->(page) { pagy_url_for(@pagy, page) }
527
- )
528
- ```
135
+ - **Rich text:** after installing Action Text and Lexxy, use
136
+ `form.field(:body, as: :rich_text)`. Lexxy owns editor behavior and uploads.
137
+ - **Direct upload:** `form.dropzone` requires configured Active Storage and its
138
+ direct-upload route. The native file input remains the submission source.
139
+ - **Pagy:** pass a Pagy object to `Pagination(pagy: @pagy)`, or use the manual
140
+ declaration API. Pagy is optional.
141
+ - **Remote command palette:** pass `search_url:` and return
142
+ `CommandPalette::Results` HTML with the same stable ID. Scope every query on
143
+ the server.
529
144
 
530
- This replaces the 1.x `nk_pagy_nav(@pagy)` helper without bringing the old global helper layer into Phlex composition.
145
+ ## Turbo responses
531
146
 
532
- ## Conventional interaction recipes
147
+ | Outcome | Response |
148
+ | ------------------------------ | -------------------------------------------------------------- |
149
+ | Successful HTML mutation | Redirect with `303 See Other` |
150
+ | Invalid mutation | Render the same model and form with `422 Unprocessable Entity` |
151
+ | Frame response | Return the same stable frame ID |
152
+ | Multiple changed regions | Return a request-scoped Turbo Stream and keep an HTML branch |
153
+ | Other sessions need the update | Broadcast |
533
154
 
534
- Use the packaged recipes for complete application flows:
155
+ Use Rails DOM helpers for frame IDs. A matching frame ID is part of the
156
+ response contract; a page-level text assertion cannot prove Turbo can apply the
157
+ response. See [Hotwire](hotwire.md) and the focused
158
+ [interaction patterns](patterns/).
535
159
 
536
- - [Queryable collections](patterns/queryable_collection.md) for GET filters, sorting, pagination, and one results frame.
537
- - [Resource forms](patterns/resource_form.md) for model-backed create/update flows and 422 validation responses.
538
- - [Destructive actions](patterns/destructive_action.md) for reviewed dialogs, compact confirmation, and 303 redirects.
539
- - [Flash and toast](patterns/flash_and_toast.md) for one server-feedback path across Turbo and HTML.
540
- - [Inline edit](patterns/inline_edit.md) for stable resource frames and Cancel behavior.
160
+ ## Upgrade verification
541
161
 
542
- These recipes are conventions rather than new client-side abstractions. Rails owns the request and policy, Hotwire owns transport and replacement, and Nitro owns the rendered UI contract.
162
+ For a Nitro Kit 1.x application, follow the dedicated
163
+ [migration guide](migration_1_to_2.md). It owns the upgrade smoke-test setup,
164
+ legacy inventory, Doctor review, and browser acceptance checklist.
@@ -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`. Compose the installed Phlex Kit and keep
28
- routes, records, authorization, queries, DOM IDs, and server responses in the
29
- application.
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