nitro_kit 0.9.0 → 2.0.0.alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/plugins/marketplace.json +20 -0
  3. data/CHANGELOG.md +172 -0
  4. data/LICENSE +28 -0
  5. data/README.md +22 -15
  6. data/STYLE_GUIDE.md +425 -0
  7. data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
  8. data/app/assets/stylesheets/nitro_kit.css +7239 -0
  9. data/app/components/nitro_kit/accordion.rb +125 -65
  10. data/app/components/nitro_kit/alert.rb +80 -43
  11. data/app/components/nitro_kit/app_navigation.rb +307 -0
  12. data/app/components/nitro_kit/app_shell.rb +227 -0
  13. data/app/components/nitro_kit/appearance_bootstrap.rb +146 -0
  14. data/app/components/nitro_kit/appearance_picker.rb +156 -0
  15. data/app/components/nitro_kit/auth_shell.rb +34 -0
  16. data/app/components/nitro_kit/avatar.rb +85 -29
  17. data/app/components/nitro_kit/avatar_stack.rb +155 -9
  18. data/app/components/nitro_kit/badge.rb +43 -81
  19. data/app/components/nitro_kit/button.rb +180 -103
  20. data/app/components/nitro_kit/button_group.rb +63 -11
  21. data/app/components/nitro_kit/button_to.rb +69 -0
  22. data/app/components/nitro_kit/card.rb +42 -45
  23. data/app/components/nitro_kit/checkbox.rb +115 -62
  24. data/app/components/nitro_kit/checkbox_group.rb +131 -22
  25. data/app/components/nitro_kit/choice.rb +55 -0
  26. data/app/components/nitro_kit/combobox.rb +292 -105
  27. data/app/components/nitro_kit/command_palette.rb +412 -0
  28. data/app/components/nitro_kit/component.rb +322 -46
  29. data/app/components/nitro_kit/container.rb +32 -0
  30. data/app/components/nitro_kit/control_group.rb +45 -0
  31. data/app/components/nitro_kit/danger_zone.rb +78 -0
  32. data/app/components/nitro_kit/data_section.rb +106 -0
  33. data/app/components/nitro_kit/details_table.rb +229 -0
  34. data/app/components/nitro_kit/dialog.rb +257 -83
  35. data/app/components/nitro_kit/dropdown.rb +335 -122
  36. data/app/components/nitro_kit/dropzone.rb +326 -0
  37. data/app/components/nitro_kit/empty_state.rb +99 -0
  38. data/app/components/nitro_kit/field.rb +461 -236
  39. data/app/components/nitro_kit/field_group.rb +15 -8
  40. data/app/components/nitro_kit/fieldset.rb +37 -39
  41. data/app/components/nitro_kit/flex.rb +49 -0
  42. data/app/components/nitro_kit/form_builder.rb +313 -72
  43. data/app/components/nitro_kit/form_section.rb +79 -0
  44. data/app/components/nitro_kit/grid.rb +34 -0
  45. data/app/components/nitro_kit/icon.rb +54 -25
  46. data/app/components/nitro_kit/input.rb +97 -10
  47. data/app/components/nitro_kit/label.rb +22 -6
  48. data/app/components/nitro_kit/layout_options.rb +7 -0
  49. data/app/components/nitro_kit/page_header.rb +76 -0
  50. data/app/components/nitro_kit/pagination.rb +355 -69
  51. data/app/components/nitro_kit/pagination_bar.rb +90 -0
  52. data/app/components/nitro_kit/progressive_image.rb +181 -0
  53. data/app/components/nitro_kit/radio_button.rb +77 -55
  54. data/app/components/nitro_kit/radio_button_group.rb +107 -36
  55. data/app/components/nitro_kit/responsive_value.rb +94 -0
  56. data/app/components/nitro_kit/rich_text_area.rb +31 -0
  57. data/app/components/nitro_kit/select.rb +129 -64
  58. data/app/components/nitro_kit/settings_layout.rb +152 -0
  59. data/app/components/nitro_kit/sheet.rb +202 -0
  60. data/app/components/nitro_kit/stat_grid.rb +75 -0
  61. data/app/components/nitro_kit/switch.rb +108 -51
  62. data/app/components/nitro_kit/table.rb +237 -45
  63. data/app/components/nitro_kit/tabs.rb +168 -63
  64. data/app/components/nitro_kit/textarea.rb +81 -10
  65. data/app/components/nitro_kit/toast.rb +211 -80
  66. data/app/components/nitro_kit/toolbar.rb +75 -0
  67. data/app/components/nitro_kit/tooltip.rb +235 -34
  68. data/app/components/nitro_kit/typeset.rb +28 -0
  69. data/app/javascript/controllers/nk/app_shell_controller.js +224 -0
  70. data/app/javascript/controllers/nk/appearance_controller.js +73 -0
  71. data/app/javascript/controllers/nk/avatar_controller.js +15 -0
  72. data/app/javascript/controllers/nk/button_controller.js +56 -0
  73. data/app/javascript/controllers/nk/checkable_controller.js +41 -0
  74. data/app/javascript/controllers/nk/combobox_controller.js +314 -82
  75. data/app/javascript/controllers/nk/command_palette_controller.js +249 -0
  76. data/app/javascript/controllers/nk/dialog_controller.js +14 -10
  77. data/app/javascript/controllers/nk/dropdown_controller.js +106 -58
  78. data/app/javascript/controllers/nk/dropzone/direct_upload.js +61 -0
  79. data/app/javascript/controllers/nk/dropzone/file_rules.js +77 -0
  80. data/app/javascript/controllers/nk/dropzone/form_submit_lock.js +30 -0
  81. data/app/javascript/controllers/nk/dropzone_controller.js +482 -0
  82. data/app/javascript/controllers/nk/overlay_position.js +37 -0
  83. data/app/javascript/controllers/nk/progressive_image_controller.js +116 -0
  84. data/app/javascript/controllers/nk/tabs_controller.js +135 -23
  85. data/app/javascript/controllers/nk/toast_controller.js +112 -45
  86. data/app/javascript/controllers/nk/tooltip_controller.js +12 -47
  87. data/config/importmap.rb +2 -0
  88. data/config/locales/en.yml +98 -0
  89. data/docs/agent_guide.md +139 -0
  90. data/docs/agent_native_spec.md +422 -0
  91. data/docs/component_contracts.md +263 -0
  92. data/docs/customization.md +595 -0
  93. data/docs/hotwire.md +94 -0
  94. data/docs/initialization_prompt.md +36 -0
  95. data/docs/migration_1_to_2.md +318 -0
  96. data/docs/new_app_strategy.md +22 -0
  97. data/docs/patterns/application_foundation.md +136 -0
  98. data/docs/patterns/crud_resource.md +144 -0
  99. data/docs/patterns/destructive_action.md +105 -0
  100. data/docs/patterns/flash_and_toast.md +57 -0
  101. data/docs/patterns/inline_edit.md +59 -0
  102. data/docs/patterns/queryable_collection.md +187 -0
  103. data/docs/patterns/resource_form.md +126 -0
  104. data/docs/rails_conventions.md +95 -0
  105. data/docs/rails_integration.md +514 -0
  106. data/lib/generators/nitro_kit/install_generator.rb +48 -0
  107. data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +67 -0
  108. data/lib/nitro_kit/engine.rb +20 -0
  109. data/lib/nitro_kit/installation.rb +671 -0
  110. data/lib/nitro_kit/migration_inventory.rb +345 -0
  111. data/lib/nitro_kit/upgrade_smoke_test.rb +403 -0
  112. data/lib/nitro_kit/version.rb +1 -1
  113. data/lib/nitro_kit.rb +5 -42
  114. data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +42 -0
  115. data/lib/tasks/nitro_kit_tasks.rake +79 -4
  116. data/plugins/nitro-kit/.codex-plugin/plugin.json +28 -0
  117. data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +67 -0
  118. data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +4 -0
  119. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +60 -0
  120. data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +4 -0
  121. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +62 -0
  122. data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +4 -0
  123. data/src/stylesheets/nitro_kit/components/accordion.css +119 -0
  124. data/src/stylesheets/nitro_kit/components/alert.css +82 -0
  125. data/src/stylesheets/nitro_kit/components/app_navigation.css +156 -0
  126. data/src/stylesheets/nitro_kit/components/app_shell.css +605 -0
  127. data/src/stylesheets/nitro_kit/components/appearance_picker.css +117 -0
  128. data/src/stylesheets/nitro_kit/components/auth_shell.css +8 -0
  129. data/src/stylesheets/nitro_kit/components/avatar.css +81 -0
  130. data/src/stylesheets/nitro_kit/components/avatar_stack.css +80 -0
  131. data/src/stylesheets/nitro_kit/components/badge.css +76 -0
  132. data/src/stylesheets/nitro_kit/components/button.css +283 -0
  133. data/src/stylesheets/nitro_kit/components/button_group.css +53 -0
  134. data/src/stylesheets/nitro_kit/components/button_to.css +6 -0
  135. data/src/stylesheets/nitro_kit/components/card.css +70 -0
  136. data/src/stylesheets/nitro_kit/components/checkbox.css +160 -0
  137. data/src/stylesheets/nitro_kit/components/checkbox_group.css +61 -0
  138. data/src/stylesheets/nitro_kit/components/combobox.css +148 -0
  139. data/src/stylesheets/nitro_kit/components/command_palette.css +255 -0
  140. data/src/stylesheets/nitro_kit/components/container.css +25 -0
  141. data/src/stylesheets/nitro_kit/components/control_group.css +149 -0
  142. data/src/stylesheets/nitro_kit/components/danger_zone.css +54 -0
  143. data/src/stylesheets/nitro_kit/components/data_section.css +40 -0
  144. data/src/stylesheets/nitro_kit/components/details_table.css +53 -0
  145. data/src/stylesheets/nitro_kit/components/dialog.css +80 -0
  146. data/src/stylesheets/nitro_kit/components/dropdown.css +152 -0
  147. data/src/stylesheets/nitro_kit/components/dropzone.css +198 -0
  148. data/src/stylesheets/nitro_kit/components/empty_state.css +53 -0
  149. data/src/stylesheets/nitro_kit/components/field.css +79 -0
  150. data/src/stylesheets/nitro_kit/components/field_group.css +6 -0
  151. data/src/stylesheets/nitro_kit/components/fieldset.css +28 -0
  152. data/src/stylesheets/nitro_kit/components/flex.css +455 -0
  153. data/src/stylesheets/nitro_kit/components/form_section.css +71 -0
  154. data/src/stylesheets/nitro_kit/components/grid.css +308 -0
  155. data/src/stylesheets/nitro_kit/components/icon.css +26 -0
  156. data/src/stylesheets/nitro_kit/components/input.css +80 -0
  157. data/src/stylesheets/nitro_kit/components/label.css +9 -0
  158. data/src/stylesheets/nitro_kit/components/layout.css +440 -0
  159. data/src/stylesheets/nitro_kit/components/page_header.css +61 -0
  160. data/src/stylesheets/nitro_kit/components/pagination.css +119 -0
  161. data/src/stylesheets/nitro_kit/components/pagination_bar.css +41 -0
  162. data/src/stylesheets/nitro_kit/components/palette.css +113 -0
  163. data/src/stylesheets/nitro_kit/components/progressive_image.css +139 -0
  164. data/src/stylesheets/nitro_kit/components/radio_button.css +127 -0
  165. data/src/stylesheets/nitro_kit/components/radio_button_group.css +143 -0
  166. data/src/stylesheets/nitro_kit/components/rich_text_area.css +21 -0
  167. data/src/stylesheets/nitro_kit/components/select.css +70 -0
  168. data/src/stylesheets/nitro_kit/components/settings_layout.css +100 -0
  169. data/src/stylesheets/nitro_kit/components/sheet.css +100 -0
  170. data/src/stylesheets/nitro_kit/components/stat_grid.css +51 -0
  171. data/src/stylesheets/nitro_kit/components/switch.css +152 -0
  172. data/src/stylesheets/nitro_kit/components/table.css +116 -0
  173. data/src/stylesheets/nitro_kit/components/tabs.css +138 -0
  174. data/src/stylesheets/nitro_kit/components/textarea.css +39 -0
  175. data/src/stylesheets/nitro_kit/components/toast.css +131 -0
  176. data/src/stylesheets/nitro_kit/components/toolbar.css +78 -0
  177. data/src/stylesheets/nitro_kit/components/tooltip.css +124 -0
  178. data/src/stylesheets/nitro_kit/components/typeset.css +191 -0
  179. data/src/stylesheets/nitro_kit/layers.css +7 -0
  180. data/src/stylesheets/nitro_kit/reset.css +192 -0
  181. data/src/stylesheets/nitro_kit/tokens.css +198 -0
  182. metadata +182 -51
  183. data/Rakefile +0 -8
  184. data/app/assets/tailwind/application.css +0 -49
  185. data/app/components/nitro_kit/datepicker.rb +0 -9
  186. data/app/helpers/nitro_kit/accordion_helper.rb +0 -9
  187. data/app/helpers/nitro_kit/alert_helper.rb +0 -11
  188. data/app/helpers/nitro_kit/avatar_helper.rb +0 -13
  189. data/app/helpers/nitro_kit/badge_helper.rb +0 -13
  190. data/app/helpers/nitro_kit/button_group_helper.rb +0 -9
  191. data/app/helpers/nitro_kit/button_helper.rb +0 -49
  192. data/app/helpers/nitro_kit/card_helper.rb +0 -9
  193. data/app/helpers/nitro_kit/checkbox_helper.rb +0 -50
  194. data/app/helpers/nitro_kit/combobox_helper.rb +0 -9
  195. data/app/helpers/nitro_kit/datepicker_helper.rb +0 -9
  196. data/app/helpers/nitro_kit/dialog_helper.rb +0 -9
  197. data/app/helpers/nitro_kit/dropdown_helper.rb +0 -9
  198. data/app/helpers/nitro_kit/field_group_helper.rb +0 -9
  199. data/app/helpers/nitro_kit/field_helper.rb +0 -9
  200. data/app/helpers/nitro_kit/fieldset_helper.rb +0 -9
  201. data/app/helpers/nitro_kit/form_helper.rb +0 -13
  202. data/app/helpers/nitro_kit/icon_helper.rb +0 -9
  203. data/app/helpers/nitro_kit/input_helper.rb +0 -37
  204. data/app/helpers/nitro_kit/label_helper.rb +0 -19
  205. data/app/helpers/nitro_kit/pagination_helper.rb +0 -46
  206. data/app/helpers/nitro_kit/radio_button_helper.rb +0 -23
  207. data/app/helpers/nitro_kit/select_helper.rb +0 -24
  208. data/app/helpers/nitro_kit/switch_helper.rb +0 -9
  209. data/app/helpers/nitro_kit/table_helper.rb +0 -9
  210. data/app/helpers/nitro_kit/tabs_helper.rb +0 -9
  211. data/app/helpers/nitro_kit/textarea_helper.rb +0 -9
  212. data/app/helpers/nitro_kit/toast_helper.rb +0 -25
  213. data/app/helpers/nitro_kit/tooltip_helper.rb +0 -9
  214. data/app/javascript/controllers/nk/accordion_controller.js +0 -18
  215. data/app/javascript/controllers/nk/datepicker_controller.js +0 -5
  216. data/app/javascript/controllers/nk/switch_controller.js +0 -32
  217. data/lib/generators/nitro_kit/component_generator.rb +0 -84
  218. data/lib/nitro_kit/schema_builder.rb +0 -129
  219. data/lib/nitro_kit/variants.rb +0 -21
@@ -0,0 +1,514 @@
1
+ # Rails and Hotwire integration
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.
4
+
5
+ ## Installation and assets
6
+
7
+ Add the 2.0 alpha prerelease to the application's Gemfile:
8
+
9
+ ```ruby
10
+ gem "nitro_kit", "2.0.0.alpha.2"
11
+ ```
12
+
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.
19
+
20
+ ```sh
21
+ bundle install
22
+ bin/rails generate nitro_kit:install
23
+ ```
24
+
25
+ Load the static stylesheet before application styles through the Rails asset pipeline:
26
+
27
+ ```erb
28
+ <%= stylesheet_link_tag "nitro_kit", "application", "data-turbo-track": "reload" %>
29
+ ```
30
+
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:
83
+
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
92
+
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:
116
+
117
+ ```js
118
+ import { application } from "controllers/application";
119
+ import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
120
+
121
+ eagerLoadControllersFrom("controllers", application);
122
+ ```
123
+
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.
127
+
128
+ ## Appearance and content security policy
129
+
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.
131
+
132
+ ```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
147
+
148
+ body do
149
+ render NitroKit::AppearancePicker.new(
150
+ id: "application-appearance",
151
+ label: "Appearance"
152
+ )
153
+ yield
154
+ end
155
+ end
156
+ end
157
+ end
158
+ ```
159
+
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:
163
+
164
+ ```text
165
+ script-src 'self' 'sha256-Vcime4euWSeYtHSfjYjqz/XhRyzMcLpn6Ip2LlaHleY='
166
+ ```
167
+
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.
259
+
260
+ ## Model-backed forms
261
+
262
+ Include the Rails helpers a Phlex component actually uses, then select `NitroKit::FormBuilder` explicitly:
263
+
264
+ ```ruby
265
+ class RegistrationForm < Phlex::HTML
266
+ include Phlex::Rails::Helpers::DOMID
267
+ include Phlex::Rails::Helpers::FormWith
268
+ include Phlex::Rails::Helpers::Routes
269
+ include Phlex::Rails::Helpers::TurboFrameTag
270
+
271
+ def initialize(registration)
272
+ @registration = registration
273
+ end
274
+
275
+ 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
433
+ end
434
+ end
435
+ 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
+ end
461
+ ```
462
+
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.
476
+
477
+ ## Pagy pagination
478
+
479
+ Keep the collection query in the controller and pass Pagy's result directly to Pagination:
480
+
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
+ ```
501
+
502
+ This replaces the 1.x `nk_pagy_nav(@pagy)` helper without bringing the old global helper layer into Phlex composition.
503
+
504
+ ## Conventional interaction recipes
505
+
506
+ Use the packaged recipes for complete application flows:
507
+
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.
513
+
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.
@@ -0,0 +1,48 @@
1
+ require "rails/generators"
2
+ require "nitro_kit/installation"
3
+
4
+ module NitroKit
5
+ class InstallGenerator < Rails::Generators::Base
6
+ class_option :prompt, type: :boolean, default: true,
7
+ desc: "Offer to copy the initialization prompt"
8
+ class_option :copy_prompt, type: :boolean, default: false,
9
+ desc: "Copy the initialization prompt without asking"
10
+
11
+ def install_agent_guidance
12
+ changes = installation.install
13
+
14
+ changes.each do |path, status|
15
+ say_status(status == :written ? :create : :identical, path)
16
+ end
17
+ end
18
+
19
+ def show_diagnostics
20
+ say ""
21
+ installation.checks.each do |check|
22
+ color = { pass: :green, warn: :yellow, fail: :red }.fetch(check.status)
23
+ say_status(check.status.to_s.upcase, "#{check.label}: #{check.detail}", color)
24
+ end
25
+ end
26
+
27
+ def offer_initialization_prompt
28
+ return copy_initialization_prompt if options[:copy_prompt]
29
+ return unless options[:prompt] && $stdin.tty?
30
+ return unless yes?("Copy the Nitro Kit 2 initialization prompt to the clipboard? [y/N]")
31
+
32
+ copy_initialization_prompt
33
+ end
34
+
35
+ private
36
+ def installation
37
+ @installation ||= Installation.new(destination_root)
38
+ end
39
+
40
+ def copy_initialization_prompt
41
+ command = installation.copy_prompt
42
+ say_status :copy, "initialization prompt via #{command}", :green
43
+ rescue RuntimeError => error
44
+ say_status :warning, error.message, :yellow
45
+ say "Run `bin/rails nitro_kit:prompt` to print the prompt instead."
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,67 @@
1
+ require "rails/generators"
2
+
3
+ module NitroKit
4
+ class UpgradeSmokeTestsGenerator < Rails::Generators::Base
5
+ TestFile = Data.define(:content, :prerequisite, :remedy)
6
+
7
+ TESTS = {
8
+ "test/integration/nitro_kit_upgrade_smoke_test.rb" => TestFile.new(
9
+ prerequisite: "test/test_helper.rb",
10
+ remedy: "Install Rails' Minitest test infrastructure, then rerun this generator.",
11
+ content: <<~RUBY
12
+ require "test_helper"
13
+ require "nitro_kit/upgrade_smoke_test"
14
+
15
+ class NitroKitUpgradeSmokeTest < NitroKit::UpgradeSmokeTest
16
+ private
17
+ def prepare_nitro_kit_upgrade_smoke_test
18
+ # This endpoint inherits ApplicationController callbacks. Use host helpers here, for example:
19
+ # sign_in users(:owner)
20
+ # select_account accounts(:primary)
21
+ end
22
+ end
23
+ RUBY
24
+ ),
25
+ "test/system/nitro_kit_upgrade_smoke_test.rb" => TestFile.new(
26
+ prerequisite: "test/application_system_test_case.rb",
27
+ remedy: "Run `bin/rails generate system_test`, configure a browser driver, then rerun this generator.",
28
+ content: <<~RUBY
29
+ require "application_system_test_case"
30
+ require "nitro_kit/upgrade_smoke_test"
31
+
32
+ class NitroKitUpgradeSmokeSystemTest < ApplicationSystemTestCase
33
+ include NitroKit::UpgradeSmokeSystemTests
34
+
35
+ private
36
+ def prepare_nitro_kit_upgrade_smoke_test
37
+ # This endpoint inherits ApplicationController callbacks. Use host browser helpers here, for example:
38
+ # sign_in_as users(:owner)
39
+ # select_account accounts(:primary)
40
+ end
41
+ end
42
+ RUBY
43
+ )
44
+ }.freeze
45
+
46
+ def install_upgrade_smoke_tests
47
+ TESTS.each do |path, test_file|
48
+ if File.exist?(destination_path(path))
49
+ say_status :skip, "#{path} already exists", :yellow
50
+ next
51
+ end
52
+
53
+ unless File.exist?(destination_path(test_file.prerequisite))
54
+ say_status :skip, "#{path}: missing #{test_file.prerequisite}. #{test_file.remedy}", :yellow
55
+ next
56
+ end
57
+
58
+ create_file(path, test_file.content)
59
+ end
60
+ end
61
+
62
+ private
63
+ def destination_path(path)
64
+ File.join(destination_root, path)
65
+ end
66
+ end
67
+ end
@@ -1,4 +1,24 @@
1
1
  module NitroKit
2
2
  class Engine < ::Rails::Engine
3
+ IMPORTMAP_PATH = root.join("config/importmap.rb")
4
+ JAVASCRIPT_PATHS = [ root.join("app/javascript") ].freeze
5
+
6
+ initializer "nitro_kit.assets" do |app|
7
+ next unless app.config.respond_to?(:assets)
8
+
9
+ JAVASCRIPT_PATHS.each do |path|
10
+ app.config.assets.paths << path unless app.config.assets.paths.include?(path)
11
+ end
12
+ end
13
+
14
+ initializer "nitro_kit.importmap", before: "importmap" do |app|
15
+ next unless app.config.respond_to?(:importmap)
16
+
17
+ app.config.importmap.paths << IMPORTMAP_PATH unless app.config.importmap.paths.include?(IMPORTMAP_PATH)
18
+
19
+ JAVASCRIPT_PATHS.each do |path|
20
+ app.config.importmap.cache_sweepers << path unless app.config.importmap.cache_sweepers.include?(path)
21
+ end
22
+ end
3
23
  end
4
24
  end