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.
- checksums.yaml +4 -4
- data/.agents/plugins/marketplace.json +20 -0
- data/CHANGELOG.md +172 -0
- data/LICENSE +28 -0
- data/README.md +22 -15
- data/STYLE_GUIDE.md +425 -0
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
- data/app/assets/stylesheets/nitro_kit.css +7239 -0
- data/app/components/nitro_kit/accordion.rb +125 -65
- data/app/components/nitro_kit/alert.rb +80 -43
- data/app/components/nitro_kit/app_navigation.rb +307 -0
- data/app/components/nitro_kit/app_shell.rb +227 -0
- data/app/components/nitro_kit/appearance_bootstrap.rb +146 -0
- data/app/components/nitro_kit/appearance_picker.rb +156 -0
- data/app/components/nitro_kit/auth_shell.rb +34 -0
- data/app/components/nitro_kit/avatar.rb +85 -29
- data/app/components/nitro_kit/avatar_stack.rb +155 -9
- data/app/components/nitro_kit/badge.rb +43 -81
- data/app/components/nitro_kit/button.rb +180 -103
- data/app/components/nitro_kit/button_group.rb +63 -11
- data/app/components/nitro_kit/button_to.rb +69 -0
- data/app/components/nitro_kit/card.rb +42 -45
- data/app/components/nitro_kit/checkbox.rb +115 -62
- data/app/components/nitro_kit/checkbox_group.rb +131 -22
- data/app/components/nitro_kit/choice.rb +55 -0
- data/app/components/nitro_kit/combobox.rb +292 -105
- data/app/components/nitro_kit/command_palette.rb +412 -0
- data/app/components/nitro_kit/component.rb +322 -46
- data/app/components/nitro_kit/container.rb +32 -0
- data/app/components/nitro_kit/control_group.rb +45 -0
- data/app/components/nitro_kit/danger_zone.rb +78 -0
- data/app/components/nitro_kit/data_section.rb +106 -0
- data/app/components/nitro_kit/details_table.rb +229 -0
- data/app/components/nitro_kit/dialog.rb +257 -83
- data/app/components/nitro_kit/dropdown.rb +335 -122
- data/app/components/nitro_kit/dropzone.rb +326 -0
- data/app/components/nitro_kit/empty_state.rb +99 -0
- data/app/components/nitro_kit/field.rb +461 -236
- data/app/components/nitro_kit/field_group.rb +15 -8
- data/app/components/nitro_kit/fieldset.rb +37 -39
- data/app/components/nitro_kit/flex.rb +49 -0
- data/app/components/nitro_kit/form_builder.rb +313 -72
- data/app/components/nitro_kit/form_section.rb +79 -0
- data/app/components/nitro_kit/grid.rb +34 -0
- data/app/components/nitro_kit/icon.rb +54 -25
- data/app/components/nitro_kit/input.rb +97 -10
- data/app/components/nitro_kit/label.rb +22 -6
- data/app/components/nitro_kit/layout_options.rb +7 -0
- data/app/components/nitro_kit/page_header.rb +76 -0
- data/app/components/nitro_kit/pagination.rb +355 -69
- data/app/components/nitro_kit/pagination_bar.rb +90 -0
- data/app/components/nitro_kit/progressive_image.rb +181 -0
- data/app/components/nitro_kit/radio_button.rb +77 -55
- data/app/components/nitro_kit/radio_button_group.rb +107 -36
- data/app/components/nitro_kit/responsive_value.rb +94 -0
- data/app/components/nitro_kit/rich_text_area.rb +31 -0
- data/app/components/nitro_kit/select.rb +129 -64
- data/app/components/nitro_kit/settings_layout.rb +152 -0
- data/app/components/nitro_kit/sheet.rb +202 -0
- data/app/components/nitro_kit/stat_grid.rb +75 -0
- data/app/components/nitro_kit/switch.rb +108 -51
- data/app/components/nitro_kit/table.rb +237 -45
- data/app/components/nitro_kit/tabs.rb +168 -63
- data/app/components/nitro_kit/textarea.rb +81 -10
- data/app/components/nitro_kit/toast.rb +211 -80
- data/app/components/nitro_kit/toolbar.rb +75 -0
- data/app/components/nitro_kit/tooltip.rb +235 -34
- data/app/components/nitro_kit/typeset.rb +28 -0
- data/app/javascript/controllers/nk/app_shell_controller.js +224 -0
- data/app/javascript/controllers/nk/appearance_controller.js +73 -0
- data/app/javascript/controllers/nk/avatar_controller.js +15 -0
- data/app/javascript/controllers/nk/button_controller.js +56 -0
- data/app/javascript/controllers/nk/checkable_controller.js +41 -0
- data/app/javascript/controllers/nk/combobox_controller.js +314 -82
- data/app/javascript/controllers/nk/command_palette_controller.js +249 -0
- data/app/javascript/controllers/nk/dialog_controller.js +14 -10
- data/app/javascript/controllers/nk/dropdown_controller.js +106 -58
- data/app/javascript/controllers/nk/dropzone/direct_upload.js +61 -0
- data/app/javascript/controllers/nk/dropzone/file_rules.js +77 -0
- data/app/javascript/controllers/nk/dropzone/form_submit_lock.js +30 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +482 -0
- data/app/javascript/controllers/nk/overlay_position.js +37 -0
- data/app/javascript/controllers/nk/progressive_image_controller.js +116 -0
- data/app/javascript/controllers/nk/tabs_controller.js +135 -23
- data/app/javascript/controllers/nk/toast_controller.js +112 -45
- data/app/javascript/controllers/nk/tooltip_controller.js +12 -47
- data/config/importmap.rb +2 -0
- data/config/locales/en.yml +98 -0
- data/docs/agent_guide.md +139 -0
- data/docs/agent_native_spec.md +422 -0
- data/docs/component_contracts.md +263 -0
- data/docs/customization.md +595 -0
- data/docs/hotwire.md +94 -0
- data/docs/initialization_prompt.md +36 -0
- data/docs/migration_1_to_2.md +318 -0
- data/docs/new_app_strategy.md +22 -0
- data/docs/patterns/application_foundation.md +136 -0
- data/docs/patterns/crud_resource.md +144 -0
- data/docs/patterns/destructive_action.md +105 -0
- data/docs/patterns/flash_and_toast.md +57 -0
- data/docs/patterns/inline_edit.md +59 -0
- data/docs/patterns/queryable_collection.md +187 -0
- data/docs/patterns/resource_form.md +126 -0
- data/docs/rails_conventions.md +95 -0
- data/docs/rails_integration.md +514 -0
- data/lib/generators/nitro_kit/install_generator.rb +48 -0
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +67 -0
- data/lib/nitro_kit/engine.rb +20 -0
- data/lib/nitro_kit/installation.rb +671 -0
- data/lib/nitro_kit/migration_inventory.rb +345 -0
- data/lib/nitro_kit/upgrade_smoke_test.rb +403 -0
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/nitro_kit.rb +5 -42
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +42 -0
- data/lib/tasks/nitro_kit_tasks.rake +79 -4
- data/plugins/nitro-kit/.codex-plugin/plugin.json +28 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +67 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +60 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +62 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +4 -0
- data/src/stylesheets/nitro_kit/components/accordion.css +119 -0
- data/src/stylesheets/nitro_kit/components/alert.css +82 -0
- data/src/stylesheets/nitro_kit/components/app_navigation.css +156 -0
- data/src/stylesheets/nitro_kit/components/app_shell.css +605 -0
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +117 -0
- data/src/stylesheets/nitro_kit/components/auth_shell.css +8 -0
- data/src/stylesheets/nitro_kit/components/avatar.css +81 -0
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +80 -0
- data/src/stylesheets/nitro_kit/components/badge.css +76 -0
- data/src/stylesheets/nitro_kit/components/button.css +283 -0
- data/src/stylesheets/nitro_kit/components/button_group.css +53 -0
- data/src/stylesheets/nitro_kit/components/button_to.css +6 -0
- data/src/stylesheets/nitro_kit/components/card.css +70 -0
- data/src/stylesheets/nitro_kit/components/checkbox.css +160 -0
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +61 -0
- data/src/stylesheets/nitro_kit/components/combobox.css +148 -0
- data/src/stylesheets/nitro_kit/components/command_palette.css +255 -0
- data/src/stylesheets/nitro_kit/components/container.css +25 -0
- data/src/stylesheets/nitro_kit/components/control_group.css +149 -0
- data/src/stylesheets/nitro_kit/components/danger_zone.css +54 -0
- data/src/stylesheets/nitro_kit/components/data_section.css +40 -0
- data/src/stylesheets/nitro_kit/components/details_table.css +53 -0
- data/src/stylesheets/nitro_kit/components/dialog.css +80 -0
- data/src/stylesheets/nitro_kit/components/dropdown.css +152 -0
- data/src/stylesheets/nitro_kit/components/dropzone.css +198 -0
- data/src/stylesheets/nitro_kit/components/empty_state.css +53 -0
- data/src/stylesheets/nitro_kit/components/field.css +79 -0
- data/src/stylesheets/nitro_kit/components/field_group.css +6 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +28 -0
- data/src/stylesheets/nitro_kit/components/flex.css +455 -0
- data/src/stylesheets/nitro_kit/components/form_section.css +71 -0
- data/src/stylesheets/nitro_kit/components/grid.css +308 -0
- data/src/stylesheets/nitro_kit/components/icon.css +26 -0
- data/src/stylesheets/nitro_kit/components/input.css +80 -0
- data/src/stylesheets/nitro_kit/components/label.css +9 -0
- data/src/stylesheets/nitro_kit/components/layout.css +440 -0
- data/src/stylesheets/nitro_kit/components/page_header.css +61 -0
- data/src/stylesheets/nitro_kit/components/pagination.css +119 -0
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +41 -0
- data/src/stylesheets/nitro_kit/components/palette.css +113 -0
- data/src/stylesheets/nitro_kit/components/progressive_image.css +139 -0
- data/src/stylesheets/nitro_kit/components/radio_button.css +127 -0
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +143 -0
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +21 -0
- data/src/stylesheets/nitro_kit/components/select.css +70 -0
- data/src/stylesheets/nitro_kit/components/settings_layout.css +100 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +100 -0
- data/src/stylesheets/nitro_kit/components/stat_grid.css +51 -0
- data/src/stylesheets/nitro_kit/components/switch.css +152 -0
- data/src/stylesheets/nitro_kit/components/table.css +116 -0
- data/src/stylesheets/nitro_kit/components/tabs.css +138 -0
- data/src/stylesheets/nitro_kit/components/textarea.css +39 -0
- data/src/stylesheets/nitro_kit/components/toast.css +131 -0
- data/src/stylesheets/nitro_kit/components/toolbar.css +78 -0
- data/src/stylesheets/nitro_kit/components/tooltip.css +124 -0
- data/src/stylesheets/nitro_kit/components/typeset.css +191 -0
- data/src/stylesheets/nitro_kit/layers.css +7 -0
- data/src/stylesheets/nitro_kit/reset.css +192 -0
- data/src/stylesheets/nitro_kit/tokens.css +198 -0
- metadata +182 -51
- data/Rakefile +0 -8
- data/app/assets/tailwind/application.css +0 -49
- data/app/components/nitro_kit/datepicker.rb +0 -9
- data/app/helpers/nitro_kit/accordion_helper.rb +0 -9
- data/app/helpers/nitro_kit/alert_helper.rb +0 -11
- data/app/helpers/nitro_kit/avatar_helper.rb +0 -13
- data/app/helpers/nitro_kit/badge_helper.rb +0 -13
- data/app/helpers/nitro_kit/button_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/button_helper.rb +0 -49
- data/app/helpers/nitro_kit/card_helper.rb +0 -9
- data/app/helpers/nitro_kit/checkbox_helper.rb +0 -50
- data/app/helpers/nitro_kit/combobox_helper.rb +0 -9
- data/app/helpers/nitro_kit/datepicker_helper.rb +0 -9
- data/app/helpers/nitro_kit/dialog_helper.rb +0 -9
- data/app/helpers/nitro_kit/dropdown_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_helper.rb +0 -9
- data/app/helpers/nitro_kit/fieldset_helper.rb +0 -9
- data/app/helpers/nitro_kit/form_helper.rb +0 -13
- data/app/helpers/nitro_kit/icon_helper.rb +0 -9
- data/app/helpers/nitro_kit/input_helper.rb +0 -37
- data/app/helpers/nitro_kit/label_helper.rb +0 -19
- data/app/helpers/nitro_kit/pagination_helper.rb +0 -46
- data/app/helpers/nitro_kit/radio_button_helper.rb +0 -23
- data/app/helpers/nitro_kit/select_helper.rb +0 -24
- data/app/helpers/nitro_kit/switch_helper.rb +0 -9
- data/app/helpers/nitro_kit/table_helper.rb +0 -9
- data/app/helpers/nitro_kit/tabs_helper.rb +0 -9
- data/app/helpers/nitro_kit/textarea_helper.rb +0 -9
- data/app/helpers/nitro_kit/toast_helper.rb +0 -25
- data/app/helpers/nitro_kit/tooltip_helper.rb +0 -9
- data/app/javascript/controllers/nk/accordion_controller.js +0 -18
- data/app/javascript/controllers/nk/datepicker_controller.js +0 -5
- data/app/javascript/controllers/nk/switch_controller.js +0 -32
- data/lib/generators/nitro_kit/component_generator.rb +0 -84
- data/lib/nitro_kit/schema_builder.rb +0 -129
- 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
|
data/lib/nitro_kit/engine.rb
CHANGED
|
@@ -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
|