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,36 @@
1
+ # Initialize Nitro Kit 2 in this Rails application
2
+
3
+ Finish the application-specific Nitro Kit 2 setup. Do not use Nitro Kit 1.x
4
+ APIs or examples.
5
+
6
+ 1. Run `bundle show nitro_kit` and confirm the resolved version begins with
7
+ `2.`.
8
+ 2. Read `docs/agent_guide.md` from that installed gem, then read the locally
9
+ installed `nitro-kit-rails`, `nitro-kit-hotwire`, and `nitro-kit-ui` skills.
10
+ 3. Inspect the application before editing. Preserve established application
11
+ conventions unless they conflict with the requested Nitro Kit 2 setup.
12
+ 4. Ensure an application base Phlex component includes `NitroKit` once and
13
+ product components inherit from it.
14
+ 5. Re-run `bin/rails generate nitro_kit:install`. Ensure its application layout
15
+ setup has one appearance bootstrap before every stylesheet, then optional
16
+ third-party base styles, the optional Tailwind adapter, `nitro_kit`, compiled
17
+ Tailwind, and application token overrides in that order. Add flash toast
18
+ rendering when the application uses it.
19
+ 6. Ensure Turbo and Stimulus are wired and the normal Stimulus loader can
20
+ discover the gem-owned `nk--*` controllers. Never copy Nitro components,
21
+ helpers, or controllers into the application.
22
+ 7. Remove confirmed Nitro Kit 1.x shadows such as application-owned
23
+ `NitroKit` components, `nk_*` helpers, or `controllers/nk` only when this
24
+ task is authorized to migrate the application.
25
+ 8. Use ordinary Rails routes, models, forms, and server-rendered HTML. Follow
26
+ the installed Rails and Hotwire guidance for new work. During a migration,
27
+ replace an existing control only when Nitro Kit 2 has a genuine semantic
28
+ and behavioral equivalent. Otherwise preserve it as application-owned
29
+ Rails and semantic HTML; never downgrade specialized behavior or retain
30
+ copied Nitro Kit 1.x source as the fallback.
31
+ 9. Run `bin/rails nitro_kit:doctor`, fix actionable failures, and run the
32
+ application's relevant tests.
33
+
34
+ Report what you changed, any existing convention you deliberately preserved,
35
+ any unsupported control recorded as a Nitro Kit coverage gap, and any warning
36
+ that still needs a product decision.
@@ -0,0 +1,318 @@
1
+ # Migrating a Nitro Kit 1.x application
2
+
3
+ Treat a 1.x migration as a product-flow review, not a helper rename. Nitro Kit
4
+ 2 deliberately removed copied components, `nk_*` helpers, application-owned
5
+ `controllers/nk`, and unrestricted utility-class customization.
6
+
7
+ ## Install the 2.0 alpha before migrating
8
+
9
+ Add the 2.0 alpha prerelease to the application's Gemfile:
10
+
11
+ ```ruby
12
+ gem "nitro_kit", "2.0.0.alpha.2"
13
+ ```
14
+
15
+ Bundler records the exact released version in `Gemfile.lock`; commit `Gemfile`
16
+ and `Gemfile.lock` together. Before upgrading during the migration, review the
17
+ changelog, run `bundle update nitro_kit`, rerun the installer and doctor, and
18
+ exercise the converted flows before committing the updated lockfile.
19
+ Production applications should use the released gem and a committed lockfile
20
+ rather than a moving Git branch.
21
+
22
+ ## Inventory behavior before editing
23
+
24
+ 1. List every `nk_*` helper, `NitroKit::*` component, copied Nitro source file,
25
+ and `controllers/nk` controller.
26
+ 2. Group usage by user flow: authentication, settings, collection browsing,
27
+ mobile navigation, editing, destructive actions, notifications, and
28
+ copy/share controls.
29
+ 3. Record behavior that must survive: native element, submitted method and
30
+ parameters, Turbo target, accessible name and description, focus behavior,
31
+ narrow-screen presentation, empty/error state, and visual density.
32
+ 4. Capture representative wide and narrow screenshots before conversion.
33
+
34
+ If the Nitro Kit MCP catalog is connected, search it by workflow after this
35
+ inventory — for example `mobile transcript navigation`, `settings form`, or
36
+ `empty query results`. Do not search only for an old component name. MCP can
37
+ deliver complete compositions; the installed gem remains the authority for
38
+ the free component contract.
39
+
40
+ ## Select semantics before atoms
41
+
42
+ Map each flow to the highest-level matching 2.x component first:
43
+
44
+ | Existing need | Begin with |
45
+ | --------------------------------------- | --------------------------------------- |
46
+ | Sign-in or recovery card | `AuthShell` |
47
+ | Application navigation | `AppShell`, `AppNavigation` |
48
+ | Mobile contextual navigation or details | `Sheet` |
49
+ | Settings screen | `SettingsLayout`, `FormSection` |
50
+ | Empty collection card | `EmptyState` |
51
+ | Data collection | `DataSection`, `Table`, `PaginationBar` |
52
+ | Destructive settings | `DangerZone`, `Dialog`, `ButtonTo` |
53
+ | Joined copy or filter controls | `ControlGroup` |
54
+
55
+ Only then replace remaining atoms. Common direct mappings include:
56
+
57
+ - `nk_button_link_to` → `NitroKit::Button.new(..., href:)`
58
+ - `nk_button_to` → `NitroKit::ButtonTo.new(..., href:, method:)`
59
+ - `nk_form_with` → Rails `form_with(..., builder: NitroKit::FormBuilder)`
60
+ - block-wrapped tooltips → a linked Button trigger or Tooltip `as: :custom`
61
+ - copied mobile Sidebar → `Sheet`, not a plain disclosure
62
+
63
+ Do not preserve a Card merely because 1.x used one. Empty states,
64
+ authentication shells, settings regions, and data sections have stronger
65
+ semantics and more useful responsive behavior.
66
+
67
+ ## Move ERB collections and yielded content into compound declarations
68
+
69
+ The ERB below is representative application-owned migration input, not a
70
+ Nitro Kit 2 API. Nitro Kit 2 has no ERB component bridge. Move the collection
71
+ to the Phlex component, then declare entries only inside their owning compound
72
+ region. Content formerly yielded by a partial belongs directly inside the
73
+ matching `panel` or `content` block.
74
+
75
+ ### AppNavigation
76
+
77
+ ERB source — the call site owns the destination collection and the partial
78
+ iterates it:
79
+
80
+ ```erb
81
+ <% destinations = [["Home", root_path], ["Projects", projects_path]] %>
82
+ <%= render "app_navigation", label: "Primary", destinations: destinations %>
83
+
84
+ <%# _app_navigation.html.erb %>
85
+ <nav aria-label="<%= label %>">
86
+ <ul>
87
+ <% destinations.each do |text, href| %>
88
+ <li><%= link_to text, href %></li>
89
+ <% end %>
90
+ </ul>
91
+ </nav>
92
+ ```
93
+
94
+ Phlex destination — declare the collection before `AppNavigation`; consume it
95
+ inside the required `body` collection region:
96
+
97
+ ```ruby
98
+ destinations = [["Home", root_path], ["Projects", projects_path]]
99
+
100
+ render NitroKit::AppNavigation.new(label: "Primary") do |navigation|
101
+ navigation.body do
102
+ destinations.each_with_index do |(text, href), index|
103
+ navigation.item(text, href:, current: index.zero?)
104
+ end
105
+ end
106
+ end
107
+ ```
108
+
109
+ The executable minimal version is on the
110
+ [`AppNavigation` gallery page](/gallery/components/app-navigation#example-app-navigation-minimal).
111
+
112
+ ### Dialog
113
+
114
+ ERB source — the call-site block is yielded inside the partial's panel:
115
+
116
+ ```erb
117
+ <%= render "dialog", id: "transcript-details", title: "Transcript details" do %>
118
+ <p>The transcript was recorded at 09:42 UTC.</p>
119
+ <% end %>
120
+
121
+ <%# _dialog.html.erb %>
122
+ <button command="show-modal" commandfor="<%= id %>-panel">Details</button>
123
+ <dialog id="<%= id %>-panel">
124
+ <h2><%= title %></h2>
125
+ <%= yield %>
126
+ </dialog>
127
+ ```
128
+
129
+ Phlex destination — trigger and panel declarations live inside `Dialog`; the
130
+ former yielded content lives inside `panel`:
131
+
132
+ ```ruby
133
+ render NitroKit::Dialog.new(id: "transcript-details") do |dialog|
134
+ dialog.trigger("Details")
135
+ dialog.panel(title: "Transcript details") do
136
+ p { "The transcript was recorded at 09:42 UTC." }
137
+ end
138
+ end
139
+ ```
140
+
141
+ Placement belongs to the parent. In the conversion that exposed this rule, a
142
+ `Flex` containing **Redact** and **Permalink** was followed by a `Dialog`
143
+ sibling, so the Dialog trigger started a second line. Put the Dialog root
144
+ inside the same no-wrap action cluster:
145
+
146
+ ```ruby
147
+ Flex(dir: :row, gap: 1, align: :center, wrap: :nowrap) do
148
+ Button("Redact", size: :sm, variant: :destructive)
149
+ Button("Permalink", href: transcript_path(transcript), size: :sm)
150
+
151
+ Dialog(id: dom_id(transcript, :details)) do |dialog|
152
+ dialog.trigger("Details", size: :sm)
153
+ dialog.panel(title: "Transcript details") do
154
+ render UI::TranscriptDetails.new(transcript)
155
+ end
156
+ end
157
+ end
158
+ ```
159
+
160
+ The gallery runs this structure at narrow widths in
161
+ [`Narrow transcript actions`](/gallery/components/dialog#example-dialog-narrow-action-cluster).
162
+
163
+ ### Sheet
164
+
165
+ ERB source — the partial yields contextual content into its side panel:
166
+
167
+ ```erb
168
+ <%= render "sheet", id: "transcript-prompts", title: "Prompts" do %>
169
+ <%= render "prompts", prompts: @prompts %>
170
+ <% end %>
171
+
172
+ <%# _sheet.html.erb %>
173
+ <button command="show-modal" commandfor="<%= id %>-panel">Prompts</button>
174
+ <dialog id="<%= id %>-panel">
175
+ <h2><%= title %></h2>
176
+ <%= yield %>
177
+ </dialog>
178
+ ```
179
+
180
+ Phlex destination — declare the collection before `Sheet`; render it only
181
+ inside the `panel` content slot:
182
+
183
+ ```ruby
184
+ prompts = transcript.prompts.map { |prompt| [prompt.title, prompt_path(prompt)] }
185
+
186
+ render NitroKit::Sheet.new(id: "transcript-prompts", side: :left) do |sheet|
187
+ sheet.trigger("Prompts", icon: :list)
188
+ sheet.panel(title: "Transcript prompts") do
189
+ render NitroKit::AppNavigation.new(label: "Transcript prompts") do |navigation|
190
+ navigation.body do
191
+ prompts.each do |text, href|
192
+ navigation.item(text, href:)
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ ```
199
+
200
+ See the executable
201
+ [`Sheet` collection example](/gallery/components/sheet#example-sheet-constructions).
202
+
203
+ ### SettingsLayout
204
+
205
+ ERB source — the call site supplies both the navigation collection and yielded
206
+ settings content:
207
+
208
+ ```erb
209
+ <% sections = [["Profile", profile_settings_path], ["Security", security_settings_path]] %>
210
+ <%= render "settings_layout", sections: sections do %>
211
+ <%= render "profile_form" %>
212
+ <% end %>
213
+
214
+ <%# _settings_layout.html.erb %>
215
+ <nav aria-label="Settings">
216
+ <% sections.each do |text, href| %>
217
+ <%= link_to text, href %>
218
+ <% end %>
219
+ </nav>
220
+ <main><%= yield %></main>
221
+ ```
222
+
223
+ Phlex destination — navigation entries stay inside `navigation`; the former
224
+ yield lives inside the one `content` region:
225
+
226
+ ```ruby
227
+ sections = [["Profile", profile_settings_path], ["Security", security_settings_path]]
228
+
229
+ render NitroKit::SettingsLayout.new do |layout|
230
+ layout.navigation(label: "Settings") do
231
+ sections.each_with_index do |(text, href), index|
232
+ layout.item(text, href:, current: index.zero?)
233
+ end
234
+ end
235
+ layout.content { render UI::ProfileForm.new(profile) }
236
+ end
237
+ ```
238
+
239
+ See the executable
240
+ [`SettingsLayout` minimal example](/gallery/components/settings-layout#example-settings-layout-cardinality-states).
241
+
242
+ ## Preserve unsupported behavior honestly
243
+
244
+ When no equivalent exists, keep semantic Rails or HTML under the application
245
+ namespace and report the missing capability. Do not retain copied 1.x source,
246
+ downgrade a specialized control, or hide the gap behind a generic component.
247
+
248
+ Tooltip custom triggers are the explicit composition path for an existing
249
+ focusable control. Forward every yielded boundary to that actual control:
250
+
251
+ ```ruby
252
+ render NitroKit::Tooltip.new(id: "revoke-help", content: "Revokes access immediately") do |tooltip|
253
+ tooltip.trigger(as: :custom) do |attributes|
254
+ render NitroKit::ButtonTo.new(
255
+ "Revoke",
256
+ href: token_path(token),
257
+ method: :delete,
258
+ variant: :destructive,
259
+ button_html: attributes.html,
260
+ button_aria: attributes.aria,
261
+ button_data: attributes.data
262
+ )
263
+ end
264
+ end
265
+ ```
266
+
267
+ ## Verify fidelity
268
+
269
+ Install and run Nitro Kit's focused host-integration acceptance flow:
270
+
271
+ ```sh
272
+ bin/rails generate nitro_kit:upgrade_smoke_tests
273
+ bin/rails test test/integration/nitro_kit_upgrade_smoke_test.rb
274
+ bin/rails test test/system/nitro_kit_upgrade_smoke_test.rb
275
+ ```
276
+
277
+ The generator does not overwrite existing tests. It generates only files
278
+ supported by the host's Rails Minitest and system-test setup and prints setup
279
+ guidance for skipped files. The tests use the currently bundled gem and cover
280
+ the shared upgrade boundary — browser-submitted Turbo validation and mutation,
281
+ Dialog and Sheet, layout-owned Rails flash, Turbo Frame identity, redirects,
282
+ and post-mutation Phlex rendering. Their collision-checked route exists only
283
+ during each test and is restored afterward; they add no production route or
284
+ component source. Keep application-specific migration tests for inventoried
285
+ product behavior alongside them.
286
+
287
+ The endpoint deliberately inherits `ApplicationController` callbacks. If the
288
+ application requires authentication or current-account state, fill in the
289
+ generated `prepare_nitro_kit_upgrade_smoke_test` methods with the same sign-in
290
+ and account-selection helpers used by ordinary integration and system tests.
291
+ Extend those application-owned classes rather than changing gem test support
292
+ or skipping host callbacks.
293
+
294
+ Run focused request and component tests, then compare the converted flows in a
295
+ browser at wide and narrow widths. Exercise keyboard focus, dialogs and sheets,
296
+ Turbo submissions, errors, empty states, light/dark appearance, and dense
297
+ metadata. A green request suite does not prove that a tooltip, off-canvas
298
+ panel, interactive Card treatment, or responsive composition survived.
299
+
300
+ Finish by deleting copied components, helpers, and controllers; run
301
+ `bin/rails nitro_kit:doctor`; and record every remaining application-owned
302
+ fallback as either intentional product UI or a Nitro Kit coverage gap.
303
+
304
+ Doctor inventories only concrete Nitro Kit 1.x conventions: `nk_*` helpers,
305
+ generated files under `app/components/nitro_kit`, controllers under
306
+ `app/javascript/controllers/nk`, the old Floating UI and combobox packages,
307
+ and `tailwind_merge`. Every finding includes a file and replacement. Its
308
+ disposition is:
309
+
310
+ - `migrated` — no remaining occurrence in that category.
311
+ - `unresolved` — a known 1.x integration still needs its documented 2.x
312
+ replacement or removal.
313
+ - `application-owned` — custom or unsupported behavior must be preserved under
314
+ an application namespace, not as a Nitro shadow.
315
+
316
+ The inventory deliberately does not guess from generic component, JavaScript,
317
+ or dependency names. Review application-owned product behavior separately and
318
+ keep its migration record with the application.
@@ -0,0 +1,22 @@
1
+ # New application strategy
2
+
3
+ Recommend a Rails application template for new applications, not a return to Nitro Kit's old component-copying generator.
4
+
5
+ Rails application templates are designed to configure a new app during `rails new`, can add gems, and can run generators after Bundler finishes. Nitro Kit's starter therefore remains a thin one-command entry point:
6
+
7
+ ```sh
8
+ rails new my_app -m https://nitrokit.dev/template.rb
9
+ ```
10
+
11
+ The template adds Nitro Kit and invokes `nitro_kit:install`. The generator owns
12
+ the project-local skills and `AGENTS.md`. The template does not copy Nitro
13
+ components, controllers, authentication, teams, billing, or product models.
14
+
15
+ Existing applications install the gem directly and run the setup generator.
16
+ Agent discovery, version-matched skill routing, diagnostics, and initialization
17
+ handoff are meaningful application-owned setup; component source remains
18
+ gem-owned.
19
+
20
+ An optional `nitro_kit:application` generator can later create an application-owned base: layout, `UI` namespace, shell choice, root screen, and authentication hooks. Keep that separate from installing the UI gem so teams can adopt Nitro Kit without adopting an application architecture.
21
+
22
+ Promote the template into a versioned starter application only when Nitro Kit deliberately owns opinions beyond UI and interaction conventions, such as authentication, accounts, teams, billing, jobs, mail, and deployment. Until then, the application template is easier to inspect, test, and evolve and does not impose the maintenance cost of a forked starter repository.
@@ -0,0 +1,136 @@
1
+ # A durable application foundation
2
+
3
+ Start a small authenticated application with the same records and page grammar
4
+ it will need after the second person joins. The first-user case should be the
5
+ smallest instance of the team model, not a separate personal mode that must be
6
+ replaced later.
7
+
8
+ ## Summary
9
+
10
+ - Model `User`, `Team`, and `Membership` from the first signup; role belongs to
11
+ `Membership`, and every team-owned record loads through `Current.team`.
12
+ - One hybrid `AppShell` frames the authenticated product: `AppNavigation` owns
13
+ brand and destinations, the shell `Toolbar` owns route titles and persistent
14
+ basic actions.
15
+ - One wrapper immediately inside `shell.main` owns responsive page padding for
16
+ every route; the shell owns viewport height and scrolling.
17
+ - Infrequent account destinations go after `navigation.spacer`; settings
18
+ compose with `SettingsLayout` and plain `FormSection` regions.
19
+ - When destination count warrants search, compose one `CommandPalette` in the
20
+ shell and render only routes the current membership may visit.
21
+ - Centralize cross-cutting feedback in one flash-driven toast region rendered
22
+ by the layout.
23
+
24
+ ## Use memberships from the first user
25
+
26
+ Use `User`, `Team`, and `Membership` even when signup creates exactly one of
27
+ each. Put the role on `Membership`, not `User`, because authority belongs to a
28
+ person's relationship with a team. Create the first team and owner membership
29
+ in the same transaction as signup.
30
+
31
+ Set `Current.user`, `Current.membership`, and `Current.team` from the session.
32
+ Load every team-owned record through `Current.team`; use `Current.user` for
33
+ authorship and audit fields. Start with the smallest role vocabulary the
34
+ product needs, usually owner, administrator, and member. Protect the last owner
35
+ in the domain model rather than only hiding a button.
36
+
37
+ Invitations should belong to a team, record the inviter and intended role,
38
+ expire, match the invited email, and be consumed when accepted. An existing
39
+ user accepts into a new membership; a new user completes signup and then uses
40
+ the same acceptance path.
41
+
42
+ ## Compose one authenticated frame
43
+
44
+ Use one hybrid `AppShell` for the authenticated product. Keep the brand and
45
+ primary destinations in `AppNavigation`; place route titles and persistent
46
+ basic actions in the shell `Toolbar`. One wrapper immediately inside
47
+ `shell.main` owns responsive page padding for every route. The topbar and
48
+ sidebar header should use the shell's shared height and border tokens rather
49
+ than independent padding guesses.
50
+
51
+ Put infrequent account navigation after `navigation.spacer`, near the account
52
+ controls at the bottom of the sidebar:
53
+
54
+ ```ruby
55
+ shell.navigation do
56
+ AppNavigation(label: "Workspace navigation") do |navigation|
57
+ navigation.body do
58
+ navigation.item("Inventory", href: assets_path, icon: :archive)
59
+ navigation.item("Team", href: team_path, icon: :users)
60
+ navigation.spacer
61
+ navigation.item("Settings", href: settings_profile_path, icon: :settings)
62
+ end
63
+ end
64
+ end
65
+ ```
66
+
67
+ Application code owns destinations and current-route policy. Nitro owns shell
68
+ layout, mobile disclosure, focus management, and navigation semantics.
69
+ For larger products, place one `CommandPalette` in the shell. Its native links
70
+ remain the navigation authority while Command-K or Control-K adds fast
71
+ filtering. Render the same authorized destination set the user can reach in
72
+ ordinary navigation; do not use the palette to bypass route policy.
73
+ When the destination set is too large or dynamic to render eagerly, pass
74
+ `search_url:` and return `CommandPalette::Results` from that endpoint. Keep the
75
+ same authorization scope on the initial links and every remote query.
76
+ Let the shell own viewport height and scrolling; do not add `min-height: 100vh`
77
+ to its main region or page wrapper. Put brand and destination icons through the
78
+ navigation slots so they share the same left alignment.
79
+
80
+ Team is an administration surface, not merely a roster. Include pending
81
+ invitations and the complete invite, role-change, removal, and revoke paths,
82
+ with last-owner protection in the model and authorization on every mutation.
83
+
84
+ Authentication is a standalone form surface. Inside `AuthShell`, use Rails
85
+ `form_with` with `NitroKit::FormBuilder` and put the visible fields, submit
86
+ control, and related recovery link in one `form.group`. The group owns their
87
+ vertical rhythm; `AuthShell` owns only the page container and spacing between
88
+ its major regions.
89
+
90
+ ## Keep settings plain
91
+
92
+ Use `SettingsLayout` inside the normal shell main region. Its navigation lists
93
+ stable subsections such as Profile, Notifications, Appearance, and Password;
94
+ its content renders the selected form. Use `FormSection` for genuinely distinct
95
+ form regions and ordinary whitespace or dividers between them.
96
+
97
+ Render subsection destinations as links and mark the active link with
98
+ `aria-current="page"`. They navigate between routes; Buttons and ButtonGroup
99
+ would incorrectly present them as in-page actions. Small preferences may
100
+ submit on change through a tiny application Stimulus controller that calls the
101
+ form's native `requestSubmit`. Keep a submit control in `noscript` so the form
102
+ still works without JavaScript.
103
+
104
+ The route still has one `h1` in the shell toolbar. Do not repeat “Settings” in
105
+ the page body, wrap each subsection in a Card, or give every form its own outer
106
+ padding. A toolbar Save button can submit the selected form with the native
107
+ `form:` attribute, so the action stays in the same place at narrow and wide
108
+ widths without JavaScript.
109
+
110
+ ## Centralize cross-cutting feedback
111
+
112
+ Render `Toast::FlashMessages` once in the application layout. Keep using
113
+ ordinary Rails flash and `303 See Other` redirects from controllers.
114
+
115
+ Compact destructive actions should continue to declare
116
+ `data: { turbo_confirm: "…" }` and use Turbo's native browser confirmation.
117
+ When the user needs branded review UI or more context than one sentence,
118
+ compose a dedicated native Nitro `Dialog` at the action's call site. The
119
+ browser's top layer keeps that inline dialog clear of ancestor clipping and
120
+ stacking contexts. Put record deletion on the edit route rather than adding a
121
+ danger surface to every operational show page.
122
+
123
+ ## Baseline acceptance path
124
+
125
+ Before polishing empty-state illustration or dashboard summaries, verify:
126
+
127
+ - signup or sign-in selects the current membership and team;
128
+ - another team cannot load the current team's records;
129
+ - owner, administrator, and member policy differs where intended;
130
+ - populated, empty, invalid, narrow, and destructive states work;
131
+ - settings forms preserve validation and use one content gutter;
132
+ - successful mutations redirect with `303` and invalid forms render with
133
+ `422`;
134
+ - confirmation has both cancel and confirm coverage;
135
+ - the shell, headings, tables, and forms remain usable without custom request
136
+ JavaScript.
@@ -0,0 +1,144 @@
1
+ # A complete product resource
2
+
3
+ Build CRUD as one coherent product surface, not independent generated screens.
4
+ Before writing the views, name the resource, tenant boundary, actor, lifecycle,
5
+ visibility rules, and states. Implement index, form, detail, destructive action,
6
+ and their tests together.
7
+
8
+ ## Summary
9
+
10
+ - Build index, form, detail, destructive action, and their tests as one
11
+ surface; name the resource, tenant boundary, actor, lifecycle, and states
12
+ before writing views.
13
+ - `AppShell(layout: :hybrid)` frames the admin area, and a `Toolbar` in
14
+ `shell.topbar` carries the route's single `h1` plus its basic actions.
15
+ - Spend hierarchy once: one route, one `h1`, an `h2` only for a genuinely
16
+ separate region, and no resource name repeated across toolbar, `PageHeader`,
17
+ `DataSection`, Card, and caption.
18
+ - Child routes add one compact icon Back link before the title instead of a
19
+ trailing Cancel action.
20
+ - One responsive padding rule on the main wrapper owns the page gutter;
21
+ data-heavy hybrid CRUD content uses the full available width.
22
+
23
+ ## Use the hybrid application frame
24
+
25
+ An authenticated admin area defaults to `AppShell(layout: :hybrid)`. Nitro Kit
26
+ owns the desktop sidebar, mobile menu button, navigation dialog, focus behavior,
27
+ and responsive transition. The application provides destinations and current
28
+ state.
29
+
30
+ Put a `Toolbar` in `shell.topbar`. Its leading region contains the route's one
31
+ `h1`; its trailing region contains basic actions such as New, Edit, Cancel,
32
+ Save, Publish, or View. A Button outside a form can submit it through the
33
+ native `form:` attribute. This keeps the same action hierarchy on narrow and
34
+ wide screens without custom JavaScript.
35
+
36
+ ```ruby
37
+ AppShell(id: "admin", layout: :hybrid) do |shell|
38
+ shell.navigation { render admin_navigation }
39
+ shell.topbar do
40
+ Toolbar do |toolbar|
41
+ toolbar.leading { h1 { page_title } }
42
+ toolbar.trailing do
43
+ Button(
44
+ "Save",
45
+ type: :submit,
46
+ form: dom_id(@post, :form),
47
+ variant: :primary
48
+ )
49
+ end
50
+ end
51
+ end
52
+ shell.main do
53
+ div(data: { ui: "admin-main" }) do
54
+ render page
55
+ end
56
+ end
57
+ end
58
+ ```
59
+
60
+ Child routes add one compact Back link before the title. Prefer an icon-only
61
+ Button with an explicit label such as `aria: { label: "Back to projects" }`.
62
+ Do not repeat that navigation as a trailing Cancel action.
63
+
64
+ The application stylesheet gives `admin-main` one responsive padding rule.
65
+ Child pages do not add another outer gutter. Keep data-heavy hybrid CRUD
66
+ content full width; constrain only a specific content-led region whose measure
67
+ benefits from it.
68
+
69
+ Do not add viewport height or another outer padding rule to `admin-main`; the
70
+ shell owns viewport geometry and the wrapper owns the one page gutter. Use the
71
+ same shell and gutter on team administration and settings routes.
72
+ Place a bottom-anchored Settings destination after `AppNavigation#spacer`, then
73
+ compose settings subsections with `SettingsLayout` and plain `FormSection`
74
+ regions. Settings destinations are links with `aria-current`, not action
75
+ Buttons. Read `application_foundation.md` for the complete application frame.
76
+
77
+ ## Spend hierarchy once
78
+
79
+ - One route, one `h1`, normally in the shell toolbar.
80
+ - Add an `h2` only for a genuinely separate region.
81
+ - Do not repeat “Posts” in the toolbar, `PageHeader`, `DataSection`, Card, and
82
+ visible table caption.
83
+ - Use `PageHeader` for a content-led introduction, not as mandatory CRUD
84
+ ceremony under an existing toolbar.
85
+ - Prefer whitespace and dividers. Use Card only for a bounded object that
86
+ benefits from its own surface. Never default to Card inside Card.
87
+
88
+ An index begins with a short introduction only if it adds useful context, then
89
+ renders its table and pagination directly. Use `DataSection` when a page has
90
+ multiple independently named datasets. At zero records, replace the data region
91
+ with one intentional `EmptyState`; keep the primary New action in the toolbar.
92
+
93
+ A new or edit page begins with `FormSection`. Use one form component for new,
94
+ edit, and invalid renders. Put the primary submit in the toolbar by setting the
95
+ button's `form:` to the form's stable DOM ID. Invalid submissions render the
96
+ same model and form with `422 Unprocessable Entity`. The toolbar owns that
97
+ action: do not render a second Save or Create submit inside the form body.
98
+
99
+ A detail page begins with status or stable metadata, then the resource itself.
100
+ Keep status inside that normal details flow instead of detaching it into a
101
+ second side panel.
102
+ Use the authenticated `show` route as the operational detail or draft preview.
103
+ Put lifecycle forms in the page and associate their toolbar buttons with
104
+ `form:`. Put destructive confirmation in one separate `DangerZone` on edit,
105
+ with a safe escape back to the record. Do not make every show page end in a
106
+ large deletion surface.
107
+
108
+ ## Model and route the lifecycle
109
+
110
+ Scope every lookup through `Current.team` or `Current.account`. Record
111
+ `Current.user` as author, creator, or publisher. If a state has provenance,
112
+ timing, or behavior, model it as a record and expose it as a noun resource:
113
+
114
+ ```ruby
115
+ namespace :admin do
116
+ resources :posts do
117
+ resource :publication,
118
+ only: %i[create destroy],
119
+ module: :posts
120
+ end
121
+ end
122
+ ```
123
+
124
+ The main controller keeps the seven REST actions. Successful mutations redirect
125
+ with `303 See Other`. Publication create and destroy invoke domain methods and
126
+ redirect. Public controllers query published records only. Ordinary Rails forms
127
+ and Turbo Drive are the default; do not add fetch code for CRUD submissions.
128
+
129
+ ## Ship the acceptance path
130
+
131
+ Request tests cover tenant isolation, public visibility, successful `303`
132
+ redirects, invalid `422` renders, pagination, and lifecycle resources. Rendering
133
+ assertions should also protect the high-level composition: hybrid AppShell,
134
+ one `h1`, navigation, toolbar action, actual form association, table or empty
135
+ state, and destructive confirmation. Add one browser test for the meaningful
136
+ end-to-end path, using Capybara waiting assertions instead of sleeps.
137
+
138
+ Assert that each primary action has one visible control. A toolbar-associated
139
+ form submit plus an identical body submit is duplication, even when both invoke
140
+ the same valid form.
141
+
142
+ Before finishing, inspect a populated index, empty index, invalid form, narrow
143
+ form, draft detail, published detail, and edit-owned destructive dialog. Remove any extra
144
+ heading, surface, wrapper, or page gutter that does not communicate information.