nitro_kit 0.8.0 → 2.0.0.alpha.1
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 +151 -0
- data/LICENSE +28 -0
- data/README.md +21 -16
- data/STYLE_GUIDE.md +425 -0
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
- data/app/assets/stylesheets/nitro_kit.css +7225 -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 +128 -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 +336 -119
- 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 +104 -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/implementation_plan.md +279 -0
- data/docs/initialization_prompt.md +36 -0
- data/docs/migration_1_to_2.md +318 -0
- data/docs/new_app_strategy.md +24 -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 +189 -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 +62 -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 +110 -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 +183 -50
- data/Rakefile +0 -8
- 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 -42
- 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 -121
- data/lib/nitro_kit/variants.rb +0 -21
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Resource form
|
|
2
|
+
|
|
3
|
+
Use one model-backed component for both the initial form and validation errors. Rails owns field names and errors; Nitro owns form presentation; the response status tells Turbo whether to replace or follow a redirect.
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
- One model-backed Phlex component renders both the initial form and its
|
|
8
|
+
validation errors.
|
|
9
|
+
- Rails owns field names and errors, Nitro owns form presentation, and the
|
|
10
|
+
response status tells Turbo what to do: 422 re-renders the invalid object,
|
|
11
|
+
303 redirects on success.
|
|
12
|
+
- In an application shell, render exactly one primary submit in the route
|
|
13
|
+
toolbar and associate it through the native `form:` attribute; do not also
|
|
14
|
+
call `form.submit` in the body.
|
|
15
|
+
- A genuinely standalone form keeps its submit inside the same `form.group` as
|
|
16
|
+
its visible fields.
|
|
17
|
+
- Parents own rhythm: stacked fields always sit inside `FieldGroup`
|
|
18
|
+
(`form.group` on the builder, or `render NitroKit::FieldGroup.new`). Fields
|
|
19
|
+
rendered as bare siblings of the `form` element stack with no gap — that is
|
|
20
|
+
an authoring mistake, not a layout the field can correct itself.
|
|
21
|
+
- Wrap the form in a Turbo Frame only when it is embedded in a larger screen
|
|
22
|
+
and needs an independent lifecycle.
|
|
23
|
+
|
|
24
|
+
## Phlex form
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
module UI
|
|
28
|
+
class ProjectForm < Phlex::HTML
|
|
29
|
+
include Phlex::Rails::Helpers::FormWith
|
|
30
|
+
|
|
31
|
+
def initialize(
|
|
32
|
+
project,
|
|
33
|
+
form_id: ActionView::RecordIdentifier.dom_id(project, :form)
|
|
34
|
+
)
|
|
35
|
+
@project = project
|
|
36
|
+
@form_id = form_id
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def view_template
|
|
40
|
+
render NitroKit::FormSection.new(
|
|
41
|
+
title: project.persisted? ? "Edit project" : "New project",
|
|
42
|
+
description: "Project details are visible to every workspace member."
|
|
43
|
+
) do |section|
|
|
44
|
+
section.form do
|
|
45
|
+
form_with(
|
|
46
|
+
model: project,
|
|
47
|
+
builder: NitroKit::FormBuilder,
|
|
48
|
+
id: form_id
|
|
49
|
+
) do |form|
|
|
50
|
+
form.group do
|
|
51
|
+
form.field(:name, required: true, autofocus: true)
|
|
52
|
+
form.field(
|
|
53
|
+
:status,
|
|
54
|
+
as: :select,
|
|
55
|
+
options: Project.statuses.keys.map do |value|
|
|
56
|
+
[ value.humanize, value ]
|
|
57
|
+
end
|
|
58
|
+
)
|
|
59
|
+
form.field(:description, as: :textarea)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
attr_reader :project, :form_id
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
In an application shell, render exactly one primary submit in the route
|
|
73
|
+
toolbar and associate it with `form_id`:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
Button(
|
|
77
|
+
project.persisted? ? "Save project" : "Create project",
|
|
78
|
+
type: :submit,
|
|
79
|
+
form: form_id,
|
|
80
|
+
variant: :primary,
|
|
81
|
+
data: { turbo_submits_with: "Saving…" }
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Do not also call `form.submit` in the form body. Use an in-form submit only
|
|
86
|
+
when the form is genuinely standalone and has no toolbar action; put that
|
|
87
|
+
submit inside the same `form.group` as its visible fields.
|
|
88
|
+
|
|
89
|
+
## Controller
|
|
90
|
+
|
|
91
|
+
Render the same invalid object with 422. Redirect successful HTML submissions with 303.
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
class ProjectsController < ApplicationController
|
|
95
|
+
def create
|
|
96
|
+
@project = Current.account.projects.build(project_params)
|
|
97
|
+
|
|
98
|
+
if @project.save
|
|
99
|
+
redirect_to @project, status: :see_other, notice: "Project created"
|
|
100
|
+
else
|
|
101
|
+
render UI::ProjectForm.new(@project), status: :unprocessable_entity
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def update
|
|
106
|
+
@project = Current.account.projects.find(params[:id])
|
|
107
|
+
|
|
108
|
+
if @project.update(project_params)
|
|
109
|
+
redirect_to @project, status: :see_other, notice: "Project updated"
|
|
110
|
+
else
|
|
111
|
+
render UI::ProjectForm.new(@project), status: :unprocessable_entity
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
def project_params
|
|
117
|
+
params.expect(project: [:name, :status, :description])
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The same response works with Turbo Drive and without it. Wrap this component in a stable Turbo Frame only when the form is embedded in a larger screen and should have an independent lifecycle; [inline edit](inline_edit.md) shows that boundary. Add a Turbo Stream branch only when success must update multiple regions instead of navigating.
|
|
123
|
+
|
|
124
|
+
## Tests
|
|
125
|
+
|
|
126
|
+
Assert 303 plus the redirect and flash on success. Assert 422, the submitted value, and the model error on failure. For an embedded form, also assert the stable frame ID. Add a system test for focus or multi-region effects, not merely to restate controller behavior.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# The Nitro Kit Rails path
|
|
2
|
+
|
|
3
|
+
Nitro Kit 2 follows one conventional Rails path so application code, UI, and
|
|
4
|
+
agent output compose predictably. Treat this as the greenfield default. In an
|
|
5
|
+
existing application, preserve established conventions unless the task
|
|
6
|
+
explicitly includes changing them.
|
|
7
|
+
|
|
8
|
+
## Model the domain with Rails
|
|
9
|
+
|
|
10
|
+
- Put business rules on domain models. Do not introduce a service, form,
|
|
11
|
+
command, or policy object until a real boundary requires one.
|
|
12
|
+
- Scope tenant-owned records through `Current.team` or `Current.account`.
|
|
13
|
+
Record `Current.user` as the actor or creator instead of using the user as
|
|
14
|
+
the tenancy boundary.
|
|
15
|
+
- For a new team-aware application, use `User`, `Team`, and `Membership` from
|
|
16
|
+
the first signup. Put roles on the membership and create the first user's
|
|
17
|
+
team plus owner membership in the same transaction. Read
|
|
18
|
+
`docs/patterns/application_foundation.md` before adding authentication,
|
|
19
|
+
invitations, team administration, or account settings.
|
|
20
|
+
- Represent meaningful lifecycle state with a record when the state has
|
|
21
|
+
identity, timing, provenance, or behavior. A published post has a
|
|
22
|
+
`Post::Publication`; it is not merely a `published` boolean.
|
|
23
|
+
- Name capability concerns with adjectives such as `Publishable` only after
|
|
24
|
+
more than one model shares the behavior. Keep the first implementation on
|
|
25
|
+
the model that owns it.
|
|
26
|
+
|
|
27
|
+
## Route nouns, not commands
|
|
28
|
+
|
|
29
|
+
Map controllers to REST resources. When an action does not fit the seven
|
|
30
|
+
standard actions, look for the missing noun:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
namespace :admin do
|
|
34
|
+
resources :posts do
|
|
35
|
+
resource :publication, only: %i[create destroy],
|
|
36
|
+
module: :posts
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`Admin::Posts::PublicationsController#create` publishes and `#destroy`
|
|
42
|
+
unpublishes. Keep controllers thin: load through the tenant boundary, invoke a
|
|
43
|
+
domain method, then render or redirect.
|
|
44
|
+
|
|
45
|
+
Use the authenticated admin `show` action as the operational detail or draft
|
|
46
|
+
preview unless the product truly has another independently addressable
|
|
47
|
+
representation. Public controllers must query only records visible to the
|
|
48
|
+
public.
|
|
49
|
+
|
|
50
|
+
## Use ordinary Rails responses
|
|
51
|
+
|
|
52
|
+
- Use `form_with` and `NitroKit::FormBuilder` for model-backed forms.
|
|
53
|
+
- Redirect successful non-GET submissions with `status: :see_other`.
|
|
54
|
+
- Render the same invalid model with `status: :unprocessable_entity`.
|
|
55
|
+
- Keep an HTML response for every Turbo-enhanced flow.
|
|
56
|
+
- Keep filtering, sorting, and pagination in GET parameters.
|
|
57
|
+
- Set flash on the server and render it through
|
|
58
|
+
`NitroKit::Toast::FlashMessages`.
|
|
59
|
+
|
|
60
|
+
## Compose CRUD as one page
|
|
61
|
+
|
|
62
|
+
For an authenticated admin or product area, prefer the hybrid `AppShell` with
|
|
63
|
+
a `Toolbar` in its topbar. The shell supplies responsive navigation disclosure;
|
|
64
|
+
the toolbar supplies the route's one `h1` and its basic actions. The application
|
|
65
|
+
owns one content gutter immediately inside the shell main region.
|
|
66
|
+
|
|
67
|
+
Do not repeat the route title in a `PageHeader`, section heading, card title,
|
|
68
|
+
and table caption. Use `PageHeader` inside an application shell only when the
|
|
69
|
+
page needs a genuinely content-led introduction beyond the toolbar title. A
|
|
70
|
+
single-table index renders the table directly. A form page begins with the
|
|
71
|
+
actual `FormSection`. A detail page begins with status or metadata. Use a Card
|
|
72
|
+
only for a bounded object that needs a surface, never as the automatic wrapper
|
|
73
|
+
for every region.
|
|
74
|
+
|
|
75
|
+
Read `docs/patterns/crud_resource.md` before building a complete resource.
|
|
76
|
+
|
|
77
|
+
## Keep the browser thin
|
|
78
|
+
|
|
79
|
+
Render HTML on the server. Use Turbo Drive by default, Frames for one stable
|
|
80
|
+
region, request Streams for multi-target responses, broadcasts for other
|
|
81
|
+
sessions, and Stimulus only for browser-owned behavior. Do not create a JSON
|
|
82
|
+
API or client-side state store for an ordinary Rails screen.
|
|
83
|
+
|
|
84
|
+
## Test the contract
|
|
85
|
+
|
|
86
|
+
Use Minitest and fixtures. Cover:
|
|
87
|
+
|
|
88
|
+
- tenant isolation and role differences;
|
|
89
|
+
- public visibility of drafts and published records;
|
|
90
|
+
- successful `303` redirects and invalid `422` renders;
|
|
91
|
+
- the noun resource that creates or removes lifecycle state;
|
|
92
|
+
- the HTML fallback before adding a system test;
|
|
93
|
+
- browser behavior with Capybara waiting assertions, never sleeps.
|
|
94
|
+
|
|
95
|
+
Prefer a small complete test over layers of factories, mocks, and helper DSLs.
|