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.
Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +229 -1
  3. data/README.md +25 -10
  4. data/STYLE_GUIDE.md +47 -5
  5. data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +3 -3
  6. data/app/assets/stylesheets/nitro_kit.css +2820 -1816
  7. data/app/components/nitro_kit/accordion.rb +2 -26
  8. data/app/components/nitro_kit/alert.rb +1 -11
  9. data/app/components/nitro_kit/app_navigation.rb +45 -11
  10. data/app/components/nitro_kit/app_shell.rb +1 -7
  11. data/app/components/nitro_kit/appearance_picker.rb +11 -12
  12. data/app/components/nitro_kit/avatar.rb +6 -1
  13. data/app/components/nitro_kit/avatar_stack.rb +3 -0
  14. data/app/components/nitro_kit/badge.rb +13 -4
  15. data/app/components/nitro_kit/button_group.rb +2 -2
  16. data/app/components/nitro_kit/checkbox.rb +7 -4
  17. data/app/components/nitro_kit/checkbox_group.rb +0 -25
  18. data/app/components/nitro_kit/combobox.rb +4 -33
  19. data/app/components/nitro_kit/command_palette.rb +42 -59
  20. data/app/components/nitro_kit/component.rb +57 -5
  21. data/app/components/nitro_kit/danger_zone.rb +5 -1
  22. data/app/components/nitro_kit/data_section.rb +28 -4
  23. data/app/components/nitro_kit/details_table.rb +19 -8
  24. data/app/components/nitro_kit/dialog.rb +23 -19
  25. data/app/components/nitro_kit/dropdown.rb +1 -6
  26. data/app/components/nitro_kit/dropzone.rb +4 -15
  27. data/app/components/nitro_kit/empty_state.rb +21 -2
  28. data/app/components/nitro_kit/field.rb +35 -11
  29. data/app/components/nitro_kit/form_builder.rb +1 -2
  30. data/app/components/nitro_kit/progressive_image.rb +2 -2
  31. data/app/components/nitro_kit/radio_button.rb +7 -4
  32. data/app/components/nitro_kit/radio_button_group.rb +0 -25
  33. data/app/components/nitro_kit/select.rb +4 -17
  34. data/app/components/nitro_kit/settings_layout.rb +17 -4
  35. data/app/components/nitro_kit/{form_section.rb → settings_section.rb} +18 -11
  36. data/app/components/nitro_kit/sheet.rb +29 -20
  37. data/app/components/nitro_kit/stat_grid.rb +18 -2
  38. data/app/components/nitro_kit/table.rb +22 -3
  39. data/app/components/nitro_kit/tabs.rb +2 -13
  40. data/app/components/nitro_kit/toast.rb +4 -2
  41. data/app/components/nitro_kit/toolbar.rb +2 -0
  42. data/app/components/nitro_kit/tooltip.rb +7 -10
  43. data/app/javascript/controllers/nk/app_shell_controller.js +3 -3
  44. data/app/javascript/controllers/nk/appearance_controller.js +6 -0
  45. data/app/javascript/controllers/nk/combobox_controller.js +51 -1
  46. data/app/javascript/controllers/nk/command_palette_controller.js +25 -11
  47. data/app/javascript/controllers/nk/dialog_controller.js +54 -0
  48. data/app/javascript/controllers/nk/dropdown_controller.js +45 -0
  49. data/app/javascript/controllers/nk/dropzone_controller.js +2 -2
  50. data/app/javascript/controllers/nk/progressive_image_controller.js +11 -7
  51. data/app/javascript/controllers/nk/tabs_controller.js +21 -2
  52. data/app/javascript/controllers/nk/tooltip_controller.js +9 -0
  53. data/config/locales/en.yml +4 -1
  54. data/docs/agent_guide.md +63 -123
  55. data/docs/agent_native_spec.md +72 -383
  56. data/docs/browser_support.md +82 -0
  57. data/docs/component_contracts.md +108 -82
  58. data/docs/customization.md +689 -91
  59. data/docs/hotwire.md +50 -76
  60. data/docs/initialization_prompt.md +26 -31
  61. data/docs/migration_1_to_2.md +88 -30
  62. data/docs/patterns/application_foundation.md +45 -119
  63. data/docs/patterns/crud_resource.md +44 -126
  64. data/docs/patterns/destructive_action.md +45 -77
  65. data/docs/patterns/flash_and_toast.md +18 -36
  66. data/docs/patterns/inline_edit.md +21 -44
  67. data/docs/patterns/queryable_collection.md +65 -161
  68. data/docs/patterns/resource_form.md +33 -81
  69. data/docs/rails_conventions.md +38 -75
  70. data/docs/rails_integration.md +96 -446
  71. data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +12 -0
  72. data/lib/nitro_kit/installation.rb +20 -7
  73. data/lib/nitro_kit/migration_inventory.rb +275 -8
  74. data/lib/nitro_kit/upgrade_smoke_test.rb +10 -12
  75. data/lib/nitro_kit/version.rb +1 -1
  76. data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +10 -3
  77. data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +4 -0
  78. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +13 -1
  79. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +32 -15
  80. data/src/stylesheets/nitro_kit/components/accordion.css +44 -44
  81. data/src/stylesheets/nitro_kit/components/alert.css +27 -36
  82. data/src/stylesheets/nitro_kit/components/app_navigation.css +92 -20
  83. data/src/stylesheets/nitro_kit/components/app_shell.css +199 -196
  84. data/src/stylesheets/nitro_kit/components/appearance_picker.css +61 -39
  85. data/src/stylesheets/nitro_kit/components/auth_shell.css +2 -2
  86. data/src/stylesheets/nitro_kit/components/avatar.css +15 -11
  87. data/src/stylesheets/nitro_kit/components/avatar_stack.css +48 -29
  88. data/src/stylesheets/nitro_kit/components/badge.css +13 -9
  89. data/src/stylesheets/nitro_kit/components/button.css +121 -79
  90. data/src/stylesheets/nitro_kit/components/button_group.css +19 -14
  91. data/src/stylesheets/nitro_kit/components/card.css +12 -6
  92. data/src/stylesheets/nitro_kit/components/checkbox.css +54 -67
  93. data/src/stylesheets/nitro_kit/components/checkbox_group.css +16 -16
  94. data/src/stylesheets/nitro_kit/components/combobox.css +39 -29
  95. data/src/stylesheets/nitro_kit/components/command_palette.css +110 -51
  96. data/src/stylesheets/nitro_kit/components/container.css +6 -6
  97. data/src/stylesheets/nitro_kit/components/control_group.css +61 -67
  98. data/src/stylesheets/nitro_kit/components/danger_zone.css +8 -8
  99. data/src/stylesheets/nitro_kit/components/data_section.css +4 -4
  100. data/src/stylesheets/nitro_kit/components/details_table.css +17 -15
  101. data/src/stylesheets/nitro_kit/components/dialog.css +8 -5
  102. data/src/stylesheets/nitro_kit/components/dropdown.css +36 -30
  103. data/src/stylesheets/nitro_kit/components/dropzone.css +78 -35
  104. data/src/stylesheets/nitro_kit/components/empty_state.css +7 -7
  105. data/src/stylesheets/nitro_kit/components/field.css +32 -28
  106. data/src/stylesheets/nitro_kit/components/field_group.css +13 -0
  107. data/src/stylesheets/nitro_kit/components/fieldset.css +8 -2
  108. data/src/stylesheets/nitro_kit/components/flex.css +1 -1
  109. data/src/stylesheets/nitro_kit/components/grid.css +1 -1
  110. data/src/stylesheets/nitro_kit/components/icon.css +5 -5
  111. data/src/stylesheets/nitro_kit/components/input.css +42 -8
  112. data/src/stylesheets/nitro_kit/components/layout.css +165 -165
  113. data/src/stylesheets/nitro_kit/components/page_header.css +7 -7
  114. data/src/stylesheets/nitro_kit/components/pagination.css +58 -49
  115. data/src/stylesheets/nitro_kit/components/pagination_bar.css +9 -9
  116. data/src/stylesheets/nitro_kit/components/palette.css +145 -77
  117. data/src/stylesheets/nitro_kit/components/progressive_image.css +45 -55
  118. data/src/stylesheets/nitro_kit/components/radio_button.css +48 -46
  119. data/src/stylesheets/nitro_kit/components/radio_button_group.css +49 -49
  120. data/src/stylesheets/nitro_kit/components/rich_text_area.css +16 -1
  121. data/src/stylesheets/nitro_kit/components/select.css +24 -15
  122. data/src/stylesheets/nitro_kit/components/settings_layout.css +26 -23
  123. data/src/stylesheets/nitro_kit/components/settings_section.css +84 -0
  124. data/src/stylesheets/nitro_kit/components/sheet.css +30 -10
  125. data/src/stylesheets/nitro_kit/components/stat_grid.css +11 -10
  126. data/src/stylesheets/nitro_kit/components/switch.css +60 -62
  127. data/src/stylesheets/nitro_kit/components/table.css +43 -35
  128. data/src/stylesheets/nitro_kit/components/tabs.css +48 -40
  129. data/src/stylesheets/nitro_kit/components/textarea.css +20 -5
  130. data/src/stylesheets/nitro_kit/components/toast.css +11 -47
  131. data/src/stylesheets/nitro_kit/components/toolbar.css +16 -41
  132. data/src/stylesheets/nitro_kit/components/tooltip.css +38 -29
  133. data/src/stylesheets/nitro_kit/components/typeset.css +150 -4
  134. data/src/stylesheets/nitro_kit/reset.css +15 -15
  135. data/src/stylesheets/nitro_kit/tokens.css +501 -79
  136. metadata +4 -4
  137. data/docs/new_app_strategy.md +0 -22
  138. data/src/stylesheets/nitro_kit/components/form_section.css +0 -71
@@ -1,118 +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 alpha 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.2"
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.
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 install
71
- a collision-checked route only for each test, then restore host routing. The
72
- endpoint inherits the host `ApplicationController` and renders through the
73
- application layout, so Nitro CSS, the appearance bootstrap, host JavaScript and
74
- CSP handling remain in the exercised path. Coverage includes browser-submitted
75
- Turbo validation and mutation, 303 redirect, layout-owned flash-to-Toast
76
- feedback, stable Turbo Frame identity, Dialog and Sheet behavior, and
77
- post-mutation Phlex rendering. No production route or application source is
78
- added.
79
-
80
- The endpoint intentionally keeps every `ApplicationController` callback. The
81
- generated classes expose a setup hook for authentication and account context;
82
- replace these example helper names with the host application's real test API:
38
+ For third-party base CSS, Tailwind, appearance setup, and token overrides, use
39
+ the canonical [stylesheet order](customization.md#stylesheet-order).
83
40
 
84
- ```ruby
85
- class NitroKitUpgradeSmokeTest < NitroKit::UpgradeSmokeTest
86
- private
87
- def prepare_nitro_kit_upgrade_smoke_test
88
- sign_in users(:owner)
89
- select_account accounts(:primary)
90
- end
91
- end
41
+ ## Stimulus
92
42
 
93
- class NitroKitUpgradeSmokeSystemTest < ApplicationSystemTestCase
94
- include NitroKit::UpgradeSmokeSystemTests
95
-
96
- private
97
- def prepare_nitro_kit_upgrade_smoke_test
98
- sign_in_as users(:owner)
99
- select_account accounts(:primary)
100
- end
101
- end
102
- ```
103
-
104
- The hook runs before the inherited requests or browser visits. These generated
105
- files are application-owned extension points: use the same session, sign-in,
106
- and account-selection path as other host tests. Do not skip callbacks or alter
107
- the gem controller, because that would bypass the integration under test.
108
-
109
- ## Stimulus and importmap
110
-
111
- Enhanced components use gem-owned Stimulus controllers, including `nk--app-shell`, `nk--appearance`, `nk--avatar`, `nk--checkable`, `nk--combobox`, `nk--dropdown`, `nk--dropzone`, `nk--progressive-image`, `nk--tabs`, `nk--toast`, and `nk--tooltip`.
112
-
113
- Accordion and Dialog are controller-free: native `details` grouping and declarative `command`/`commandfor` own their complete interaction. Dropdown uses native Popover as its source of truth and adds only menu keyboard focus; Tooltip uses CSS for hover/focus and JavaScript only for Escape dismissal. Nitro does not promise dialog light dismiss.
114
-
115
- 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:
116
45
 
117
46
  ```js
118
47
  import { application } from "controllers/application";
@@ -121,394 +50,115 @@ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
121
50
  eagerLoadControllersFrom("controllers", application);
122
51
  ```
123
52
 
124
- Nitro Kit packages no third-party JavaScript. Accordion, Dialog, date inputs, and Switch use native browser behavior and need no controllers.
125
-
126
- 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.
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).
127
57
 
128
- ## Appearance and content security policy
58
+ ## Appearance and CSP
129
59
 
130
- 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:
131
62
 
132
63
  ```ruby
133
- class ApplicationLayout < Phlex::HTML
134
- include Phlex::Rails::Layout
135
- include Phlex::Rails::Helpers::ContentSecurityPolicyNonce
136
-
137
- def view_template
138
- doctype
139
- html(lang: "en") do
140
- head do
141
- render NitroKit::AppearanceBootstrap.new(
142
- default: :system,
143
- nonce: content_security_policy_nonce
144
- )
145
- stylesheet_link_tag("nitro_kit", data: { turbo_track: "reload" })
146
- 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
147
72
 
148
- body do
149
- render NitroKit::AppearancePicker.new(
150
- id: "application-appearance",
151
- label: "Appearance"
152
- )
153
- yield
154
- end
155
- end
156
- end
73
+ body do
74
+ render NitroKit::AppearancePicker.new(
75
+ id: "application-appearance",
76
+ label: "Appearance"
77
+ )
78
+ yield
157
79
  end
158
80
  ```
159
81
 
160
- 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.
161
-
162
- 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:
163
84
 
164
85
  ```text
165
86
  script-src 'self' 'sha256-Vcime4euWSeYtHSfjYjqz/XhRyzMcLpn6Ip2LlaHleY='
166
87
  ```
167
88
 
168
- 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.
169
-
170
- 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.
171
-
172
- ## Application shells
173
-
174
- `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:
175
-
176
- ```ruby
177
- render NitroKit::AppShell.new(id: "workspace", layout: :sidebar) do |shell|
178
- shell.brand { strong { "Northstar" } }
179
-
180
- shell.navigation do
181
- render NitroKit::AppNavigation.new(label: "Primary navigation") do |navigation|
182
- navigation.body do
183
- navigation.item("Overview", href: root_path, icon: :house, current: true)
184
- navigation.item("Projects", href: projects_path, icon: :folder)
185
- navigation.spacer
186
- navigation.item("Settings", href: settings_path, icon: :settings)
187
- end
188
- end
189
- end
190
-
191
- shell.topbar do
192
- render NitroKit::Button.new("New project", href: new_project_path, variant: :primary)
193
- end
194
-
195
- shell.main { render Workspace::Dashboard.new }
196
- end
197
- ```
198
-
199
- 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.
200
-
201
- Large destination sets may add one command palette to the shell:
202
-
203
- ```ruby
204
- render NitroKit::CommandPalette.new(id: "workspace-search", label: "Search workspace…") do |palette|
205
- palette.destination("Overview", href: root_path, description: "Workspace")
206
- palette.destination("Projects", href: projects_path, description: "Workspace")
207
- palette.destination("Settings", href: settings_path, description: "Account")
208
- end
209
- ```
210
-
211
- The application remains responsible for authorization and must render only destinations the current user may visit. Native dialog commands and links provide the baseline; Stimulus adds filtering and the Command-K or Control-K shortcut. Use `shortcut: false` for any additional palette on the same document so only one component owns the global shortcut.
212
-
213
- ### Server-rendered command palette results
214
-
215
- 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 no-JavaScript baseline and the immediate first render.
216
-
217
- ```ruby
218
- render NitroKit::CommandPalette.new(
219
- id: "workspace-search",
220
- label: "Search workspace…",
221
- search_url: command_palette_results_path
222
- ) do |palette|
223
- current_user.recent_destinations.each do |destination|
224
- palette.destination(destination.name, href: destination.path, description: destination.section)
225
- end
226
- end
227
- ```
228
-
229
- 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:
230
-
231
- ```ruby
232
- # config/routes.rb
233
- resources :command_palette_results, only: :index
234
-
235
- # app/controllers/command_palette_results_controller.rb
236
- class CommandPaletteResultsController < ApplicationController
237
- def index
238
- @destinations = Current.user.destinations.search(params[:query])
239
- end
240
- end
241
- ```
242
-
243
- 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.
244
-
245
- ```erb
246
- <%# app/views/command_palette_results/index.html.erb %>
247
- <%= render NitroKit::CommandPalette::Results.new(id: "workspace-search") do |results| %>
248
- <% @destinations.each do |destination| %>
249
- <% results.destination(
250
- destination.name,
251
- href: destination.path,
252
- description: destination.section
253
- ) %>
254
- <% end %>
255
- <% end %>
256
- ```
257
-
258
- 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.
259
93
 
260
94
  ## Model-backed forms
261
95
 
262
- 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:
263
97
 
264
98
  ```ruby
265
99
  class RegistrationForm < Phlex::HTML
266
- include Phlex::Rails::Helpers::DOMID
267
100
  include Phlex::Rails::Helpers::FormWith
268
- include Phlex::Rails::Helpers::Routes
269
- include Phlex::Rails::Helpers::TurboFrameTag
270
101
 
271
102
  def initialize(registration)
272
103
  @registration = registration
273
104
  end
274
105
 
275
106
  def view_template
276
- turbo_frame_tag(dom_id(@registration, :form)) do
277
- form_with(
278
- model: @registration,
279
- url: registration_path,
280
- builder: NitroKit::FormBuilder,
281
- id: dom_id(@registration, :details)
282
- ) do |form|
283
- form.hidden_field(:source)
284
- form.group do
285
- form.field(:email, as: :email, required: true)
286
- form.field(
287
- :role,
288
- as: :select,
289
- options: [["Developer", "developer"], ["Designer", "designer"]],
290
- prompt: "Choose a role",
291
- required: true
292
- )
293
- form.field(:terms, as: :checkbox, label: "I accept the terms")
294
- form.field(:attachment, as: :file, accept: "text/plain")
295
- form.submit("Register", data: { turbo_submits_with: "Registering…" })
296
- end
297
- end
298
- end
299
- end
300
- end
301
- ```
302
-
303
- `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.
304
-
305
- `form.group` supplies the default vertical rhythm between a standalone form's
306
- visible fields, submit control, and related links. Keep hidden fields outside
307
- the group when convenient; they do not participate in layout. Use more than
308
- one group only when the form has genuinely distinct sections.
309
-
310
- 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:
311
-
312
- ```ruby
313
- form.email_field(
314
- :email,
315
- maxlength: 120,
316
- data: { action: "input->signup#validate" },
317
- aria: { describedby: "email-help" }
318
- )
319
-
320
- form.select(:role, nil) do
321
- option(value: "developer") { "Developer" }
322
- option(value: "designer") { "Designer" }
323
- end
324
- ```
325
-
326
- 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.
327
-
328
- `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:`.
329
-
330
- ### Rich text with Lexxy
331
-
332
- Lexxy is Nitro Kit's preferred Action Text editor. Once the application has
333
- installed Action Text and Lexxy, use the same builder API as every other field:
334
-
335
- ```ruby
336
- form.field(:brief, as: :rich_text, placeholder: "Describe the project")
337
- ```
338
-
339
- Nitro wraps the editor in the ordinary Field contract, so labels, descriptions,
340
- validation errors, layout, and theme tokens remain consistent. Lexxy continues
341
- to own its hidden input, attachment flow, editor behavior, prompts, and native
342
- options; pass editor-specific attributes through `control_html:`. Nitro Kit does
343
- not bundle or fork Lexxy's JavaScript.
344
-
345
- The complete builder surface includes:
346
-
347
- - `field`, `fieldset`, and `group`.
348
- - `dropzone` for native file selection with optional Active Storage direct uploads.
349
- - `select`, `radio_button`, `check_box`/`checkbox`, and `hidden_field`.
350
- - `submit` and `button`.
351
- - Rails-shaped color, date, datetime, email, file, month, number, password, phone/telephone, range, rich text, search, text, textarea, time, URL, and week fields.
352
-
353
- `submit` renders a primary submit Button named `commit`, and `button` defaults
354
- to `type: :submit`. The Rails helpers Nitro does not style — `label`,
355
- `collection_select`, `grouped_collection_select`, `collection_radio_buttons`,
356
- `collection_check_boxes`, `date_select`, and `time_zone_select` — raise and name
357
- their `form.field(as:)` equivalent instead of leaking unstyled markup.
358
-
359
- ### Mutation buttons and joined controls
360
-
361
- Navigation uses `Button.new(..., href:)`. A non-GET action uses `ButtonTo`,
362
- which renders one Rails method form and one submit Button:
363
-
364
- ```ruby
365
- render NitroKit::ButtonTo.new(
366
- "Revoke token",
367
- href: token_path(token),
368
- method: :delete,
369
- variant: :destructive,
370
- data: { turbo_confirm: "Revoke this token?" }
371
- )
372
- ```
373
-
374
- Root `html:`, `aria:`, and `data:` address the form. `button_html:`,
375
- `button_aria:`, and `button_data:` address the nested focusable Button when a
376
- composition such as Tooltip must attach attributes there. The form is
377
- layout-transparent, so ButtonTo participates in Flex, Grid, and action rows as
378
- its Button.
379
-
380
- Use `ControlGroup` when adjacent native controls intentionally share borders:
381
-
382
- ```ruby
383
- render NitroKit::ControlGroup.new(label: "Copy webhook URL") do
384
- render NitroKit::Input.new(value: webhook_url, readonly: true)
385
- render NitroKit::Button.new("Copy", type: :button, icon: :copy)
386
- end
387
- ```
388
-
389
- Direct Input, Select, and Button children keep their own values and behavior.
390
- `group.addon("https://")` adds a textual prefix, suffix, or unit. Do not use a
391
- ControlGroup merely to reduce ordinary form spacing; FieldGroup owns vertical
392
- form rhythm.
393
-
394
- ### File drops and direct uploads
395
-
396
- `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`:
397
-
398
- ```ruby
399
- form.dropzone(
400
- :attachments,
401
- label: "Upload evidence",
402
- description: "Up to three PDF files, each no larger than 5 MB.",
403
- multiple: true,
404
- accept: "application/pdf",
405
- max_files: 3,
406
- max_bytes: 5 * 1024 * 1024,
407
- required: true
408
- )
409
- ```
410
-
411
- 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.
412
-
413
- 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.
414
-
415
- ## Validation responses
416
-
417
- 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`.
418
-
419
- ```ruby
420
- def create
421
- @registration = Registration.new(registration_params)
422
- @registration.valid? ? render_success : render_errors
423
- end
424
-
425
- private
426
- def render_errors
427
- respond_to do |format|
428
- format.turbo_stream do
429
- render RegistrationStream.new(@registration), status: :unprocessable_entity
430
- end
431
- format.html do
432
- 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")
433
118
  end
434
119
  end
435
120
  end
436
- ```
437
-
438
- Keep the HTML branch. It is the progressive fallback when Turbo is unavailable.
439
-
440
- ## Turbo Frames and Streams
441
-
442
- 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.
443
-
444
- Turbo Stream responses can also be Phlex components:
445
-
446
- ```ruby
447
- class RegistrationStream < Phlex::HTML
448
- include Phlex::Rails::Helpers::DOMID
449
- include Phlex::Rails::Helpers::TurboStream
450
-
451
- def initialize(registration)
452
- @registration = registration
453
- end
454
-
455
- def view_template
456
- turbo_stream.replace(dom_id(@registration, :form)) do
457
- render RegistrationForm.new(@registration)
458
- end
459
- end
460
121
  end
461
122
  ```
462
123
 
463
- 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.
464
-
465
- The dummy application's `RailsIntegration::RegistrationForm`,
466
- `RegistrationStream`, `RegistrationSuccess`, and request/system tests are an
467
- executable reference implementation of this contract. The form submits a
468
- note. A 422 response replaces `form_registration` with the same invalid Phlex
469
- form and preserves that submitted note; a successful stream replaces the same
470
- frame with `RegistrationSuccess` and renders the submitted email and note.
471
- The tests scope those assertions to
472
- `turbo-stream > template > turbo-frame#form_registration` and, in the browser,
473
- to the live `turbo-frame#form_registration`. This matching-ID boundary is
474
- intentional: a page-level text assertion can pass while a frame response is
475
- missing its target and Turbo renders nothing.
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.
476
127
 
477
- ## Pagy pagination
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.
478
132
 
479
- Keep the collection query in the controller and pass Pagy's result directly to Pagination:
133
+ ### Optional integrations
480
134
 
481
- ```ruby
482
- class ProjectsController < ApplicationController
483
- def index
484
- @pagy, @projects = pagy(:offset, Project.order(updated_at: :desc))
485
- end
486
- end
487
- ```
488
-
489
- ```ruby
490
- Pagination(pagy: @pagy)
491
- ```
492
-
493
- 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:
494
-
495
- ```ruby
496
- Pagination(
497
- pagy: @pagy,
498
- page_url: ->(page) { pagy_url_for(@pagy, page) }
499
- )
500
- ```
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.
501
144
 
502
- This replaces the 1.x `nk_pagy_nav(@pagy)` helper without bringing the old global helper layer into Phlex composition.
145
+ ## Turbo responses
503
146
 
504
- ## 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 |
505
154
 
506
- 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/).
507
159
 
508
- - [Queryable collections](patterns/queryable_collection.md) for GET filters, sorting, pagination, and one results frame.
509
- - [Resource forms](patterns/resource_form.md) for model-backed create/update flows and 422 validation responses.
510
- - [Destructive actions](patterns/destructive_action.md) for reviewed dialogs, compact confirmation, and 303 redirects.
511
- - [Flash and toast](patterns/flash_and_toast.md) for one server-feedback path across Turbo and HTML.
512
- - [Inline edit](patterns/inline_edit.md) for stable resource frames and Cancel behavior.
160
+ ## Upgrade verification
513
161
 
514
- 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.
@@ -9,6 +9,12 @@ module NitroKit
9
9
  prerequisite: "test/test_helper.rb",
10
10
  remedy: "Install Rails' Minitest test infrastructure, then rerun this generator.",
11
11
  content: <<~RUBY
12
+ # Generated by Nitro Kit as a host-integration regression check.
13
+ #
14
+ # This exercises the bundled Nitro Kit through this application's layout,
15
+ # ApplicationController callbacks, Rails forms, Turbo, and flash rendering.
16
+ # It does not replace acceptance tests for the application's migrated flows.
17
+
12
18
  require "test_helper"
13
19
  require "nitro_kit/upgrade_smoke_test"
14
20
 
@@ -26,6 +32,12 @@ module NitroKit
26
32
  prerequisite: "test/application_system_test_case.rb",
27
33
  remedy: "Run `bin/rails generate system_test`, configure a browser driver, then rerun this generator.",
28
34
  content: <<~RUBY
35
+ # Generated by Nitro Kit as a host-integration regression check.
36
+ #
37
+ # This exercises the bundled Nitro Kit through this application's layout,
38
+ # ApplicationController callbacks, Rails forms, Turbo, and browser runtime.
39
+ # It does not replace acceptance tests for the application's migrated flows.
40
+
29
41
  require "application_system_test_case"
30
42
  require "nitro_kit/upgrade_smoke_test"
31
43