nitro_kit 2.0.0.alpha.2 → 2.0.0.alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +229 -1
- data/README.md +25 -10
- data/STYLE_GUIDE.md +47 -5
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +3 -3
- data/app/assets/stylesheets/nitro_kit.css +2820 -1816
- data/app/components/nitro_kit/accordion.rb +2 -26
- data/app/components/nitro_kit/alert.rb +1 -11
- data/app/components/nitro_kit/app_navigation.rb +45 -11
- data/app/components/nitro_kit/app_shell.rb +1 -7
- data/app/components/nitro_kit/appearance_picker.rb +11 -12
- data/app/components/nitro_kit/avatar.rb +6 -1
- data/app/components/nitro_kit/avatar_stack.rb +3 -0
- data/app/components/nitro_kit/badge.rb +13 -4
- data/app/components/nitro_kit/button_group.rb +2 -2
- data/app/components/nitro_kit/checkbox.rb +7 -4
- data/app/components/nitro_kit/checkbox_group.rb +0 -25
- data/app/components/nitro_kit/combobox.rb +4 -33
- data/app/components/nitro_kit/command_palette.rb +42 -59
- data/app/components/nitro_kit/component.rb +57 -5
- data/app/components/nitro_kit/danger_zone.rb +5 -1
- data/app/components/nitro_kit/data_section.rb +28 -4
- data/app/components/nitro_kit/details_table.rb +19 -8
- data/app/components/nitro_kit/dialog.rb +23 -19
- data/app/components/nitro_kit/dropdown.rb +1 -6
- data/app/components/nitro_kit/dropzone.rb +4 -15
- data/app/components/nitro_kit/empty_state.rb +21 -2
- data/app/components/nitro_kit/field.rb +35 -11
- data/app/components/nitro_kit/form_builder.rb +1 -2
- data/app/components/nitro_kit/progressive_image.rb +2 -2
- data/app/components/nitro_kit/radio_button.rb +7 -4
- data/app/components/nitro_kit/radio_button_group.rb +0 -25
- data/app/components/nitro_kit/select.rb +4 -17
- data/app/components/nitro_kit/settings_layout.rb +17 -4
- data/app/components/nitro_kit/{form_section.rb → settings_section.rb} +18 -11
- data/app/components/nitro_kit/sheet.rb +29 -20
- data/app/components/nitro_kit/stat_grid.rb +18 -2
- data/app/components/nitro_kit/table.rb +22 -3
- data/app/components/nitro_kit/tabs.rb +2 -13
- data/app/components/nitro_kit/toast.rb +4 -2
- data/app/components/nitro_kit/toolbar.rb +2 -0
- data/app/components/nitro_kit/tooltip.rb +7 -10
- data/app/javascript/controllers/nk/app_shell_controller.js +3 -3
- data/app/javascript/controllers/nk/appearance_controller.js +6 -0
- data/app/javascript/controllers/nk/combobox_controller.js +51 -1
- data/app/javascript/controllers/nk/command_palette_controller.js +25 -11
- data/app/javascript/controllers/nk/dialog_controller.js +54 -0
- data/app/javascript/controllers/nk/dropdown_controller.js +45 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +2 -2
- data/app/javascript/controllers/nk/progressive_image_controller.js +11 -7
- data/app/javascript/controllers/nk/tabs_controller.js +21 -2
- data/app/javascript/controllers/nk/tooltip_controller.js +9 -0
- data/config/locales/en.yml +4 -1
- data/docs/agent_guide.md +63 -123
- data/docs/agent_native_spec.md +72 -383
- data/docs/browser_support.md +82 -0
- data/docs/component_contracts.md +108 -82
- data/docs/customization.md +689 -91
- data/docs/hotwire.md +50 -76
- data/docs/initialization_prompt.md +26 -31
- data/docs/migration_1_to_2.md +88 -30
- data/docs/patterns/application_foundation.md +45 -119
- data/docs/patterns/crud_resource.md +44 -126
- data/docs/patterns/destructive_action.md +45 -77
- data/docs/patterns/flash_and_toast.md +18 -36
- data/docs/patterns/inline_edit.md +21 -44
- data/docs/patterns/queryable_collection.md +65 -161
- data/docs/patterns/resource_form.md +33 -81
- data/docs/rails_conventions.md +38 -75
- data/docs/rails_integration.md +96 -446
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +12 -0
- data/lib/nitro_kit/installation.rb +20 -7
- data/lib/nitro_kit/migration_inventory.rb +275 -8
- data/lib/nitro_kit/upgrade_smoke_test.rb +10 -12
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +10 -3
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +13 -1
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +32 -15
- data/src/stylesheets/nitro_kit/components/accordion.css +44 -44
- data/src/stylesheets/nitro_kit/components/alert.css +27 -36
- data/src/stylesheets/nitro_kit/components/app_navigation.css +92 -20
- data/src/stylesheets/nitro_kit/components/app_shell.css +199 -196
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +61 -39
- data/src/stylesheets/nitro_kit/components/auth_shell.css +2 -2
- data/src/stylesheets/nitro_kit/components/avatar.css +15 -11
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +48 -29
- data/src/stylesheets/nitro_kit/components/badge.css +13 -9
- data/src/stylesheets/nitro_kit/components/button.css +121 -79
- data/src/stylesheets/nitro_kit/components/button_group.css +19 -14
- data/src/stylesheets/nitro_kit/components/card.css +12 -6
- data/src/stylesheets/nitro_kit/components/checkbox.css +54 -67
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +16 -16
- data/src/stylesheets/nitro_kit/components/combobox.css +39 -29
- data/src/stylesheets/nitro_kit/components/command_palette.css +110 -51
- data/src/stylesheets/nitro_kit/components/container.css +6 -6
- data/src/stylesheets/nitro_kit/components/control_group.css +61 -67
- data/src/stylesheets/nitro_kit/components/danger_zone.css +8 -8
- data/src/stylesheets/nitro_kit/components/data_section.css +4 -4
- data/src/stylesheets/nitro_kit/components/details_table.css +17 -15
- data/src/stylesheets/nitro_kit/components/dialog.css +8 -5
- data/src/stylesheets/nitro_kit/components/dropdown.css +36 -30
- data/src/stylesheets/nitro_kit/components/dropzone.css +78 -35
- data/src/stylesheets/nitro_kit/components/empty_state.css +7 -7
- data/src/stylesheets/nitro_kit/components/field.css +32 -28
- data/src/stylesheets/nitro_kit/components/field_group.css +13 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +8 -2
- data/src/stylesheets/nitro_kit/components/flex.css +1 -1
- data/src/stylesheets/nitro_kit/components/grid.css +1 -1
- data/src/stylesheets/nitro_kit/components/icon.css +5 -5
- data/src/stylesheets/nitro_kit/components/input.css +42 -8
- data/src/stylesheets/nitro_kit/components/layout.css +165 -165
- data/src/stylesheets/nitro_kit/components/page_header.css +7 -7
- data/src/stylesheets/nitro_kit/components/pagination.css +58 -49
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +9 -9
- data/src/stylesheets/nitro_kit/components/palette.css +145 -77
- data/src/stylesheets/nitro_kit/components/progressive_image.css +45 -55
- data/src/stylesheets/nitro_kit/components/radio_button.css +48 -46
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +49 -49
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +16 -1
- data/src/stylesheets/nitro_kit/components/select.css +24 -15
- data/src/stylesheets/nitro_kit/components/settings_layout.css +26 -23
- data/src/stylesheets/nitro_kit/components/settings_section.css +84 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +30 -10
- data/src/stylesheets/nitro_kit/components/stat_grid.css +11 -10
- data/src/stylesheets/nitro_kit/components/switch.css +60 -62
- data/src/stylesheets/nitro_kit/components/table.css +43 -35
- data/src/stylesheets/nitro_kit/components/tabs.css +48 -40
- data/src/stylesheets/nitro_kit/components/textarea.css +20 -5
- data/src/stylesheets/nitro_kit/components/toast.css +11 -47
- data/src/stylesheets/nitro_kit/components/toolbar.css +16 -41
- data/src/stylesheets/nitro_kit/components/tooltip.css +38 -29
- data/src/stylesheets/nitro_kit/components/typeset.css +150 -4
- data/src/stylesheets/nitro_kit/reset.css +15 -15
- data/src/stylesheets/nitro_kit/tokens.css +501 -79
- metadata +4 -4
- data/docs/new_app_strategy.md +0 -22
- data/src/stylesheets/nitro_kit/components/form_section.css +0 -71
data/docs/rails_integration.md
CHANGED
|
@@ -1,118 +1,47 @@
|
|
|
1
|
-
# Rails
|
|
1
|
+
# Rails integration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Audience:** Application developers and coding agents installing Nitro Kit or
|
|
4
|
+
connecting it to Rails forms, assets, Stimulus, and Hotwire.
|
|
4
5
|
|
|
5
|
-
|
|
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
|
-
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
Pin the current prerelease:
|
|
8
13
|
|
|
9
14
|
```ruby
|
|
10
|
-
gem "nitro_kit", "2.0.0.alpha.
|
|
15
|
+
gem "nitro_kit", "2.0.0.alpha.4"
|
|
11
16
|
```
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
58
|
+
## Appearance and CSP
|
|
129
59
|
|
|
130
|
-
Render
|
|
60
|
+
Render `AppearanceBootstrap` before stylesheets. Render zero or more pickers in
|
|
61
|
+
the body:
|
|
131
62
|
|
|
132
63
|
```ruby
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
### Optional integrations
|
|
480
134
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
145
|
+
## Turbo responses
|
|
503
146
|
|
|
504
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|