nitro_kit 2.0.0.alpha.3 → 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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +179 -0
  3. data/README.md +22 -26
  4. data/STYLE_GUIDE.md +10 -0
  5. data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +3 -3
  6. data/app/assets/stylesheets/nitro_kit.css +1220 -572
  7. data/app/components/nitro_kit/accordion.rb +1 -19
  8. data/app/components/nitro_kit/alert.rb +1 -11
  9. data/app/components/nitro_kit/app_navigation.rb +44 -9
  10. data/app/components/nitro_kit/appearance_picker.rb +10 -1
  11. data/app/components/nitro_kit/badge.rb +13 -4
  12. data/app/components/nitro_kit/combobox.rb +1 -17
  13. data/app/components/nitro_kit/command_palette.rb +1 -0
  14. data/app/components/nitro_kit/dropzone.rb +1 -1
  15. data/app/components/nitro_kit/form_builder.rb +1 -1
  16. data/app/components/nitro_kit/select.rb +1 -14
  17. data/app/components/nitro_kit/tabs.rb +1 -0
  18. data/app/javascript/controllers/nk/command_palette_controller.js +4 -0
  19. data/app/javascript/controllers/nk/tabs_controller.js +13 -1
  20. data/docs/agent_guide.md +63 -138
  21. data/docs/agent_native_spec.md +72 -400
  22. data/docs/browser_support.md +77 -188
  23. data/docs/component_contracts.md +56 -36
  24. data/docs/customization.md +675 -89
  25. data/docs/hotwire.md +61 -99
  26. data/docs/initialization_prompt.md +27 -57
  27. data/docs/migration_1_to_2.md +34 -18
  28. data/docs/patterns/application_foundation.md +45 -121
  29. data/docs/patterns/crud_resource.md +44 -129
  30. data/docs/patterns/destructive_action.md +45 -102
  31. data/docs/patterns/flash_and_toast.md +18 -36
  32. data/docs/patterns/inline_edit.md +21 -44
  33. data/docs/patterns/queryable_collection.md +65 -161
  34. data/docs/patterns/resource_form.md +33 -81
  35. data/docs/rails_conventions.md +38 -78
  36. data/docs/rails_integration.md +96 -474
  37. data/lib/nitro_kit/installation.rb +11 -3
  38. data/lib/nitro_kit/migration_inventory.rb +15 -8
  39. data/lib/nitro_kit/version.rb +1 -1
  40. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +7 -0
  41. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +15 -8
  42. data/src/stylesheets/nitro_kit/components/accordion.css +4 -4
  43. data/src/stylesheets/nitro_kit/components/alert.css +20 -29
  44. data/src/stylesheets/nitro_kit/components/app_navigation.css +74 -2
  45. data/src/stylesheets/nitro_kit/components/app_shell.css +3 -3
  46. data/src/stylesheets/nitro_kit/components/appearance_picker.css +20 -8
  47. data/src/stylesheets/nitro_kit/components/auth_shell.css +2 -2
  48. data/src/stylesheets/nitro_kit/components/avatar.css +12 -8
  49. data/src/stylesheets/nitro_kit/components/avatar_stack.css +12 -10
  50. data/src/stylesheets/nitro_kit/components/badge.css +13 -9
  51. data/src/stylesheets/nitro_kit/components/button.css +42 -25
  52. data/src/stylesheets/nitro_kit/components/card.css +2 -2
  53. data/src/stylesheets/nitro_kit/components/checkbox.css +29 -46
  54. data/src/stylesheets/nitro_kit/components/checkbox_group.css +3 -3
  55. data/src/stylesheets/nitro_kit/components/combobox.css +6 -6
  56. data/src/stylesheets/nitro_kit/components/command_palette.css +62 -4
  57. data/src/stylesheets/nitro_kit/components/container.css +6 -6
  58. data/src/stylesheets/nitro_kit/components/control_group.css +9 -9
  59. data/src/stylesheets/nitro_kit/components/danger_zone.css +6 -6
  60. data/src/stylesheets/nitro_kit/components/data_section.css +4 -4
  61. data/src/stylesheets/nitro_kit/components/details_table.css +7 -5
  62. data/src/stylesheets/nitro_kit/components/dialog.css +3 -3
  63. data/src/stylesheets/nitro_kit/components/dropdown.css +7 -7
  64. data/src/stylesheets/nitro_kit/components/dropzone.css +7 -7
  65. data/src/stylesheets/nitro_kit/components/empty_state.css +4 -4
  66. data/src/stylesheets/nitro_kit/components/field.css +2 -2
  67. data/src/stylesheets/nitro_kit/components/fieldset.css +2 -2
  68. data/src/stylesheets/nitro_kit/components/flex.css +1 -1
  69. data/src/stylesheets/nitro_kit/components/grid.css +1 -1
  70. data/src/stylesheets/nitro_kit/components/icon.css +5 -5
  71. data/src/stylesheets/nitro_kit/components/input.css +33 -6
  72. data/src/stylesheets/nitro_kit/components/page_header.css +4 -4
  73. data/src/stylesheets/nitro_kit/components/pagination.css +3 -3
  74. data/src/stylesheets/nitro_kit/components/pagination_bar.css +2 -2
  75. data/src/stylesheets/nitro_kit/components/palette.css +140 -72
  76. data/src/stylesheets/nitro_kit/components/progressive_image.css +1 -1
  77. data/src/stylesheets/nitro_kit/components/radio_button.css +28 -33
  78. data/src/stylesheets/nitro_kit/components/radio_button_group.css +6 -6
  79. data/src/stylesheets/nitro_kit/components/rich_text_area.css +2 -2
  80. data/src/stylesheets/nitro_kit/components/select.css +24 -8
  81. data/src/stylesheets/nitro_kit/components/settings_layout.css +5 -5
  82. data/src/stylesheets/nitro_kit/components/settings_section.css +2 -2
  83. data/src/stylesheets/nitro_kit/components/sheet.css +10 -10
  84. data/src/stylesheets/nitro_kit/components/stat_grid.css +1 -1
  85. data/src/stylesheets/nitro_kit/components/switch.css +34 -40
  86. data/src/stylesheets/nitro_kit/components/table.css +8 -4
  87. data/src/stylesheets/nitro_kit/components/tabs.css +5 -1
  88. data/src/stylesheets/nitro_kit/components/textarea.css +20 -5
  89. data/src/stylesheets/nitro_kit/components/toast.css +11 -58
  90. data/src/stylesheets/nitro_kit/components/toolbar.css +2 -2
  91. data/src/stylesheets/nitro_kit/components/tooltip.css +4 -0
  92. data/src/stylesheets/nitro_kit/components/typeset.css +4 -4
  93. data/src/stylesheets/nitro_kit/reset.css +6 -6
  94. data/src/stylesheets/nitro_kit/tokens.css +497 -74
  95. metadata +1 -2
  96. data/docs/new_app_strategy.md +0 -22
@@ -1,147 +1,62 @@
1
- # A complete product resource
1
+ # Complete product resource
2
2
 
3
- Build CRUD as one coherent product surface, not independent generated screens.
4
- Before writing the views, name the resource, tenant boundary, actor, lifecycle,
5
- visibility rules, and states. Implement index, form, detail, destructive action,
6
- and their tests together.
3
+ **Audience:** Coding agents and developers implementing a full Rails CRUD
4
+ resource with Nitro Kit.
7
5
 
8
6
  ## Summary
9
7
 
10
- - Build index, form, detail, destructive action, and their tests as one
11
- surface; name the resource, tenant boundary, actor, lifecycle, and states
12
- before writing views.
13
- - `AppShell(layout: :hybrid)` frames the admin area, and a `Toolbar` in
14
- `shell.topbar` carries the route's single `h1` plus its basic actions.
15
- - Spend hierarchy once: one route, one `h1`, an `h2` only for a genuinely
16
- separate region, and no resource name repeated across toolbar, `PageHeader`,
17
- `DataSection`, Card, and caption.
18
- - Child routes add one compact icon Back link before the title instead of a
19
- trailing Cancel action.
20
- - At narrow widths, trailing actions stack below the Back affordance and title;
21
- do not clip the title or hide persistent actions to keep the header short.
22
- - One responsive padding rule on the main wrapper owns the page gutter;
23
- data-heavy hybrid CRUD content uses the full available width.
8
+ - Define the resource, tenant boundary, actor, lifecycle, visibility, and
9
+ states before writing views.
10
+ - Build index, form, detail, destructive action, and tests as one product
11
+ surface.
12
+ - Use one shell toolbar title and one application-owned page gutter; do not
13
+ repeat hierarchy across nested components.
14
+ - Scope every lookup through the current tenant and model meaningful lifecycle
15
+ transitions as noun resources.
24
16
 
25
- ## Use the hybrid application frame
17
+ ## Resource map
26
18
 
27
- An authenticated admin area defaults to `AppShell(layout: :hybrid)`. Nitro Kit
28
- owns the desktop sidebar, mobile menu button, navigation dialog, focus behavior,
29
- and responsive transition. The application provides destinations and current
30
- state.
19
+ Use `AppShell(layout: :hybrid)` for an authenticated product area. Put the
20
+ route's one `h1` and persistent actions in the topbar `Toolbar`. Child routes
21
+ place one compact Back link before the title. One wrapper inside `shell.main`
22
+ owns page padding; child pages add no outer gutter.
31
23
 
32
- Put a `Toolbar` in `shell.topbar`. Its leading region contains the route's one
33
- `h1`; its trailing region contains basic actions such as New, Edit, Cancel,
34
- Save, Publish, or View. A Button outside a form can submit it through the
35
- native `form:` attribute. This keeps the same action hierarchy on narrow and
36
- wide screens without custom JavaScript. Nitro Kit stacks the regions at narrow
37
- widths so a Back affordance, long title, and several actions remain legible.
24
+ | Route | Composition |
25
+ | --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
26
+ | Index | Optional short introduction, then Table or EmptyState and pagination. Use DataSection only for multiple named datasets. |
27
+ | New/Edit | One `SettingsSection` and one shared form component. A toolbar submit targets the form's stable `form:` ID. |
28
+ | Show | Status or metadata, then the resource. Keep lifecycle actions in the normal detail flow. |
29
+ | Edit destructive area | One `DangerZone` with a safe escape. Do not put permanent deletion on every show page. |
38
30
 
39
- ```ruby
40
- AppShell(id: "admin", layout: :hybrid) do |shell|
41
- shell.navigation { render admin_navigation }
42
- shell.topbar do
43
- Toolbar do |toolbar|
44
- toolbar.leading { h1 { page_title } }
45
- toolbar.trailing do
46
- Button(
47
- "Save",
48
- type: :submit,
49
- form: dom_id(@post, :form),
50
- variant: :primary
51
- )
52
- end
53
- end
54
- end
55
- shell.main do
56
- div(data: { ui: "admin-main" }) do
57
- render page
58
- end
59
- end
60
- end
61
- ```
62
-
63
- Child routes add one compact Back link before the title. Prefer an icon-only
64
- Button with an explicit label such as `aria: { label: "Back to projects" }`.
65
- Do not repeat that navigation as a trailing Cancel action.
66
-
67
- The application stylesheet gives `admin-main` one responsive padding rule.
68
- Child pages do not add another outer gutter. Keep data-heavy hybrid CRUD
69
- content full width; constrain only a specific content-led region whose measure
70
- benefits from it.
71
-
72
- Do not add viewport height or another outer padding rule to `admin-main`; the
73
- shell owns viewport geometry and the wrapper owns the one page gutter. Use the
74
- same shell and gutter on team administration and settings routes.
75
- Place a bottom-anchored Settings destination after `AppNavigation#spacer`, then
76
- compose settings subsections with `SettingsLayout` and plain `SettingsSection`
77
- regions. Settings destinations are links with `aria-current`, not action
78
- Buttons. Read `application_foundation.md` for the complete application frame.
31
+ Use one primary action. Do not render the same Save or Create action in both
32
+ the toolbar and form body. Use Card only for a bounded object that benefits
33
+ from its own surface.
79
34
 
80
- ## Spend hierarchy once
35
+ See [Resource form](resource_form.md),
36
+ [Destructive action](destructive_action.md), and
37
+ [Queryable collection](queryable_collection.md) for complete interaction
38
+ contracts.
81
39
 
82
- - One route, one `h1`, normally in the shell toolbar.
83
- - Add an `h2` only for a genuinely separate region.
84
- - Do not repeat “Posts” in the toolbar, `PageHeader`, `DataSection`, Card, and
85
- visible table caption.
86
- - Use `PageHeader` for a content-led introduction, not as mandatory CRUD
87
- ceremony under an existing toolbar.
88
- - Prefer whitespace and dividers. Use Card only for a bounded object that
89
- benefits from its own surface. Never default to Card inside Card.
40
+ ## Lifecycle and responses
90
41
 
91
- An index begins with a short introduction only if it adds useful context, then
92
- renders its table and pagination directly. Use `DataSection` when a page has
93
- multiple independently named datasets. At zero records, replace the data region
94
- with one intentional `EmptyState`; keep the primary New action in the toolbar.
95
-
96
- A new or edit page begins with `SettingsSection`. Use one form component for new,
97
- edit, and invalid renders. Put the primary submit in the toolbar by setting the
98
- button's `form:` to the form's stable DOM ID. Invalid submissions render the
99
- same model and form with `422 Unprocessable Entity`. The toolbar owns that
100
- action: do not render a second Save or Create submit inside the form body.
101
-
102
- A detail page begins with status or stable metadata, then the resource itself.
103
- Keep status inside that normal details flow instead of detaching it into a
104
- second side panel.
105
- Use the authenticated `show` route as the operational detail or draft preview.
106
- Put lifecycle forms in the page and associate their toolbar buttons with
107
- `form:`. Put destructive confirmation in one separate `DangerZone` on edit,
108
- with a safe escape back to the record. Do not make every show page end in a
109
- large deletion surface.
110
-
111
- ## Model and route the lifecycle
112
-
113
- Scope every lookup through `Current.team` or `Current.account`. Record
114
- `Current.user` as author, creator, or publisher. If a state has provenance,
115
- timing, or behavior, model it as a record and expose it as a noun resource:
42
+ Scope lookups through `Current.team` or `Current.account`. Use
43
+ `Current.user` as actor. When state has timing, provenance, or behavior, model
44
+ it as a noun resource:
116
45
 
117
46
  ```ruby
118
- namespace :admin do
119
- resources :posts do
120
- resource :publication,
121
- only: %i[create destroy],
122
- module: :posts
123
- end
47
+ resources :posts do
48
+ resource :publication, only: %i[create destroy], module: :posts
124
49
  end
125
50
  ```
126
51
 
127
- The main controller keeps the seven REST actions. Successful mutations redirect
128
- with `303 See Other`. Publication create and destroy invoke domain methods and
129
- redirect. Public controllers query published records only. Ordinary Rails forms
130
- and Turbo Drive are the default; do not add fetch code for CRUD submissions.
131
-
132
- ## Ship the acceptance path
133
-
134
- Request tests cover tenant isolation, public visibility, successful `303`
135
- redirects, invalid `422` renders, pagination, and lifecycle resources. Rendering
136
- assertions should also protect the high-level composition: hybrid AppShell,
137
- one `h1`, navigation, toolbar action, actual form association, table or empty
138
- state, and destructive confirmation. Add one browser test for the meaningful
139
- end-to-end path, using Capybara waiting assertions instead of sleeps.
52
+ Keep the main controller to REST actions. Successful mutations redirect with
53
+ `303 See Other`; invalid forms render the same model with `422 Unprocessable
54
+ Entity`. Public controllers query only publicly visible records.
140
55
 
141
- Assert that each primary action has one visible control. A toolbar-associated
142
- form submit plus an identical body submit is duplication, even when both invoke
143
- the same valid form.
56
+ ## Acceptance checklist
144
57
 
145
- Before finishing, inspect a populated index, empty index, invalid form, narrow
146
- form, draft detail, published detail, and edit-owned destructive dialog. Remove any extra
147
- heading, surface, wrapper, or page gutter that does not communicate information.
58
+ Test tenant isolation, authorization, public visibility, lifecycle resources,
59
+ pagination, `303` redirects, and `422` validation. Protect the high-level
60
+ composition: one title, one primary action, the correct form association,
61
+ Table or EmptyState, and edit-owned destructive confirmation. Inspect
62
+ populated, empty, invalid, narrow, draft, published, and destructive states.
@@ -1,82 +1,58 @@
1
1
  # Destructive action
2
2
 
3
- Use a native Nitro Dialog when the user needs to review impact or type confirmation. The dialog only owns accessible disclosure and focus behavior; a real Rails form owns the destructive request.
4
-
5
- Nitro keeps the declarative dialog command as the native path and supplies a
6
- capability-detected JavaScript bridge for supported browsers that lack Invoker
7
- Commands. The bridge changes only disclosure; it never intercepts or recreates
8
- the form request.
3
+ **Audience:** Coding agents and developers implementing delete, revoke,
4
+ archive, or similarly destructive Rails actions.
9
5
 
10
6
  ## Summary
11
7
 
12
- - Use a native Nitro `Dialog` only when the user must review impact or type a
13
- confirmation; a real Rails form owns the destructive request.
14
- - Place the reviewed deletion on the resource's edit page, not on the
15
- operational show page.
16
- - The delete form targets `_top` so a successful redirect navigates the page
17
- instead of resolving inside the surrounding frame.
18
- - Authorize and load the record on the server. The confirmation UI is not a
19
- security boundary.
20
- - When the consequence fits in one sentence, use Turbo's native `turbo_confirm`
21
- instead. Never stack both confirmation surfaces.
8
+ - Use `NitroKit::Dialog` when the user must review impact or type confirmation;
9
+ use Turbo's native browser confirmation for a one-sentence consequence.
10
+ - A real Rails form owns the request, and the server owns authorization.
11
+ - Put permanent deletion on the edit route, not the operational show route.
12
+ - Use a server-rendered review route when confirmation must work without
13
+ JavaScript or Invoker Commands.
22
14
 
23
- ## Reviewed deletion
15
+ ## Choose one confirmation path
24
16
 
25
- Place this composition on the resource's edit page. A show page is the
26
- operational home; it should not advertise permanent deletion on every visit.
17
+ | Need | Pattern |
18
+ | ----------------------------------------------- | --------------------------------------------------------------- |
19
+ | One-sentence confirmation | Real form with `data: { turbo_confirm: "Delete permanently?" }` |
20
+ | Reviewed impact or typed confirmation | `DangerZone` containing a Dialog and real form |
21
+ | Confirmation required without client JavaScript | Ordinary link to a server-rendered review page |
27
22
 
28
- ```ruby
29
- module UI
30
- class DeleteProject < Phlex::HTML
31
- include Phlex::Rails::Helpers::DOMID
32
- include Phlex::Rails::Helpers::FormWith
33
- include Phlex::Rails::Helpers::Routes
23
+ Never stack `turbo_confirm` inside a Dialog.
34
24
 
35
- def initialize(project)
36
- @project = project
37
- end
38
-
39
- def view_template
40
- render NitroKit::DangerZone.new(
41
- title: "Delete project",
42
- description: "This permanently removes the project and its activity.",
43
- id: dom_id(project, :danger_zone)
44
- ) do |zone|
45
- zone.confirmation do
46
- render NitroKit::Dialog.new(id: dom_id(project, :delete_dialog)) do |dialog|
47
- dialog.trigger("Review deletion", variant: :destructive)
48
- dialog.panel(
49
- title: "Delete #{project.name}?",
50
- description: "This action cannot be undone."
51
- ) do
52
- form_with(
53
- model: project,
54
- url: project_path(project),
55
- method: :delete,
56
- data: { turbo_frame: "_top" }
57
- ) do
58
- render NitroKit::Button.new(
59
- "Delete project",
60
- type: :submit,
61
- variant: :destructive,
62
- data: { turbo_submits_with: "Deleting…" }
63
- )
64
- end
65
- dialog.close_button(label: "Cancel deletion")
66
- end
67
- end
25
+ ```ruby
26
+ render NitroKit::DangerZone.new(
27
+ title: "Delete project",
28
+ description: "This permanently removes the project.",
29
+ id: dom_id(project, :danger_zone)
30
+ ) do |zone|
31
+ zone.confirmation do
32
+ render NitroKit::Dialog.new(id: dom_id(project, :delete_dialog)) do |dialog|
33
+ dialog.trigger("Review deletion", variant: :destructive)
34
+ dialog.panel(title: "Delete #{project.name}?") do
35
+ form_with(
36
+ model: project,
37
+ method: :delete,
38
+ data: { turbo_frame: "_top" }
39
+ ) do
40
+ render NitroKit::Button.new(
41
+ "Delete project",
42
+ type: :submit,
43
+ variant: :destructive
44
+ )
68
45
  end
69
- zone.escape NitroKit::Button.new("Keep project", href: project_path(project))
46
+ dialog.close_button(label: "Cancel deletion")
70
47
  end
71
48
  end
72
-
73
- private
74
- attr_reader :project
75
49
  end
50
+ zone.escape NitroKit::Button.new("Keep project", href: project_path(project))
76
51
  end
77
52
  ```
78
53
 
79
- The top-level target makes a successful redirect navigate the page rather than trying to render the destination inside the surrounding settings frame.
54
+ The `_top` target keeps the redirect out of a surrounding frame. The dialog is
55
+ not a security boundary; load and authorize the record on the server.
80
56
 
81
57
  ```ruby
82
58
  def destroy
@@ -86,45 +62,12 @@ def destroy
86
62
  end
87
63
  ```
88
64
 
89
- Authorize and load the record on the server even when the dialog is open. The confirmation UI is not a security boundary.
90
-
91
- ## Compact confirmation
92
-
93
- When the consequence fits in one sentence and needs no review UI, keep the ordinary request and use Turbo's native browser confirmation:
94
-
95
- ```ruby
96
- form_with(model: project, url: project_path(project), method: :delete) do
97
- render NitroKit::Button.new(
98
- "Delete",
99
- type: :submit,
100
- variant: :destructive,
101
- data: { turbo_confirm: "Delete this project permanently?" }
102
- )
103
- end
104
- ```
105
-
106
- Do not stack `turbo_confirm` inside a Dialog. Choose one confirmation surface.
107
-
108
- ## Browser fallback
109
-
110
- The reviewed flow depends on Dialog opening in the user's browser. Nitro owns
111
- the compatibility path when its packaged controller is installed, but no
112
- client-side dialog can guarantee confirmation when both JavaScript and the
113
- required native dialog invocation API are unavailable. Applications that must
114
- support that case should link to a server-rendered review route with an ordinary
115
- deletion form.
116
-
117
- `data-turbo-confirm` is a compact Turbo enhancement, not that no-JavaScript
118
- fallback. Without Turbo it does not display a confirmation, although the real
119
- form can still submit. When confirmation must be unavoidable, make review a
120
- server-owned step rather than stacking both client confirmation surfaces.
65
+ Without JavaScript, a form inside a closed Dialog is reachable only where
66
+ Invoker Commands are supported. `data-turbo-confirm` also requires Turbo. Use
67
+ the server-owned review route when confirmation must be unavoidable. See
68
+ [Browser support](../browser_support.md).
121
69
 
122
70
  ## Tests
123
71
 
124
- Request-test authorization, deletion, 303 redirect, and flash. System-test the dialog only when the reviewed flow matters: trigger opens it, Cancel closes and restores focus, and the destructive submit removes the record.
125
-
126
- The successful `DELETE` response must redirect with `303 See Other`, including
127
- when Turbo submits the form. Without JavaScript the real form still provides a
128
- request path, but a form placed only inside a closed dialog is reachable only
129
- where Invoker Commands are supported; provide an ordinary server-rendered link
130
- to a review page when deletion must remain reachable across that baseline.
72
+ Request-test authorization, mutation, flash, and the `303` redirect. For a
73
+ reviewed flow, system-test open, cancel with focus restoration, and confirm.
@@ -1,57 +1,39 @@
1
1
  # Flash and toast
2
2
 
3
- Rails flash is the server-side feedback contract. Render it once in the application layout with Nitro's toast adapter so redirects, Turbo visits, and full-page fallbacks all use the same path.
3
+ **Audience:** Coding agents and developers presenting Rails server feedback.
4
4
 
5
5
  ## Summary
6
6
 
7
- - Render `NitroKit::Toast::FlashMessages` once in the application layout; Rails
8
- flash stays the single server-side feedback contract.
9
- - `notice` maps to the default presentation, `alert` and `error` to error, and
10
- `success`, `warning`, and `info` to their matching variants.
11
- - Every toast item is Turbo-temporary so a cached page never replays old
12
- feedback, while the region itself survives and stays addressable.
13
- - Use `flash.now` when rendering the current request; a Turbo Stream that does
14
- not redirect updates the same stable notification region.
15
- - Do not introduce a client-side notification store for server outcomes.
7
+ - Rails flash is the server-side feedback contract; render
8
+ `NitroKit::Toast::FlashMessages` once in the application layout.
9
+ - Use redirect flash for navigation and `flash.now` when rendering the current
10
+ request.
11
+ - Toast items are Turbo-temporary so cached pages do not replay them; the
12
+ region keeps a stable address for stream updates.
13
+ - Do not add a client-side notification store for server outcomes.
16
14
 
17
15
  ```ruby
18
- class UI::ApplicationLayout < Phlex::HTML
19
- include Phlex::Rails::Layout
20
- include Phlex::Rails::Helpers::Flash
21
-
22
- def view_template
23
- doctype
24
- html do
25
- head do
26
- stylesheet_link_tag("nitro_kit", data: { turbo_track: "reload" })
27
- end
28
- body do
29
- render NitroKit::Toast::FlashMessages.new(flash: flash)
30
- yield
31
- end
32
- end
33
- end
16
+ body do
17
+ render NitroKit::Toast::FlashMessages.new(flash: flash)
18
+ yield
34
19
  end
35
20
  ```
36
21
 
37
- Controllers set ordinary flash while redirecting:
22
+ Controllers use ordinary Rails flash:
38
23
 
39
24
  ```ruby
40
25
  redirect_to projects_path, status: :see_other, notice: "Project created"
41
- redirect_to billing_path, status: :see_other, alert: "Payment method was declined"
42
- ```
43
-
44
- `notice` maps to the default presentation, `alert` and `error` to error, and `success`, `warning`, and `info` to their matching variants. Every toast item is Turbo-temporary so a cached page does not replay old feedback, dismissible or not. The region itself survives, so `#{toast id}-list` stays addressable.
45
26
 
46
- Use `flash.now` only when rendering in the current request:
47
-
48
- ```ruby
49
27
  flash.now[:alert] = "Import failed"
50
28
  render UI::ImportForm.new(@import), status: :unprocessable_entity
51
29
  ```
52
30
 
53
- For a request-scoped Turbo Stream that does not redirect, update a stable notification region in the same stream response. Keep the HTML branch and flash fallback. Do not introduce a client-side notification store for server outcomes.
31
+ `notice` uses the default presentation; `alert` and `error` map to error;
32
+ `success`, `warning`, and `info` map to matching variants. A request-scoped
33
+ Turbo Stream may update the same stable Toast list, but must keep an HTML
34
+ branch and flash fallback.
54
35
 
55
36
  ## Tests
56
37
 
57
- Controller tests assert the flash severity and message. One layout or integration test should prove the flash renders through `section[data-nk=toast]`. Nitro's own tests cover timers and dismissal behavior.
38
+ Assert flash severity and message in request tests. One layout or integration
39
+ test should prove rendering through `section[data-nk=toast]`.
@@ -1,59 +1,36 @@
1
1
  # Inline edit
2
2
 
3
- Wrap one complete resource region in a stable Turbo Frame. Show, edit, validation failure, success, and Cancel all return that same frame ID.
3
+ **Audience:** Coding agents and developers editing one resource region in
4
+ place with Turbo Frames.
4
5
 
5
6
  ## Summary
6
7
 
7
- - One complete resource region lives in a stable Turbo Frame; show, edit,
8
- invalid, success, and Cancel all return that same frame ID.
9
- - Invalid updates render the editing frame with 422; success redirects with 303
10
- and Turbo extracts the matching read-only frame.
11
- - The show response must contain the same `turbo_frame_tag(dom_id(record))` as
12
- the edit response.
13
- - Use a Turbo Stream only when the update also changes another region, such as
14
- a page title or a summary count.
8
+ - Wrap the complete resource region in one stable Turbo Frame.
9
+ - Show, edit, validation failure, success, and Cancel return the same frame ID.
10
+ - Invalid updates render with `422`; successful updates redirect with `303`.
11
+ - Use a Turbo Stream only when the update changes another region too.
15
12
 
16
13
  ```ruby
17
- module UI
18
- class ProjectPanel < Phlex::HTML
19
- include Phlex::Rails::Helpers::DOMID
20
- include Phlex::Rails::Helpers::TurboFrameTag
21
-
22
- def initialize(project, editing: false)
23
- @project = project
24
- @editing = editing
25
- end
26
-
27
- def view_template
28
- turbo_frame_tag(dom_id(project)) do
29
- if editing
30
- render UI::ProjectFormFields.new(project)
31
- else
32
- render NitroKit::Card.new do |card|
33
- card.title(project.name)
34
- card.body { project.description }
35
- card.footer do
36
- render NitroKit::Button.new("Edit", href: "/projects/#{project.id}/edit")
37
- end
38
- end
39
- end
40
- end
14
+ turbo_frame_tag(dom_id(project)) do
15
+ if editing
16
+ render UI::ProjectForm.new(project)
17
+ else
18
+ div do
19
+ h2 { project.name }
20
+ render NitroKit::Button.new("Edit", href: edit_project_path(project))
41
21
  end
42
-
43
- private
44
- attr_reader :project, :editing
45
22
  end
46
23
  end
47
24
  ```
48
25
 
49
- The edit form submits normally within the frame. Its Cancel link targets the resource show action, which returns the read-only frame. Invalid updates render the editing frame with 422. A successful update redirects with 303 to the resource action; Turbo follows the redirect and extracts the matching read-only frame:
50
-
51
- ```ruby
52
- redirect_to @project, status: :see_other, notice: "Project updated"
53
- ```
54
-
55
- The show response must contain `turbo_frame_tag(dom_id(project))` just like the edit response. If success renders directly instead, return frame-shaped HTML rather than labeling a bare frame as a Turbo Stream. Use a stream only when the update also changes another region, such as a page title or summary count.
26
+ The edit form submits inside the frame. Cancel links to the show action, whose
27
+ response contains the same `turbo_frame_tag(dom_id(project))`. Invalid updates
28
+ render the editing frame with `status: :unprocessable_entity`. Success redirects
29
+ with `status: :see_other`; Turbo follows it and extracts the matching read-only
30
+ frame.
56
31
 
57
32
  ## Tests
58
33
 
59
- Assert every endpoint returns the same `turbo-frame` ID. Cover invalid values remaining visible at 422 and Cancel restoring the read-only view. Add a system test when focus or scroll preservation is important.
34
+ Assert the same frame ID on every endpoint, submitted values and errors at
35
+ `422`, success at `303`, and Cancel restoring the read-only view. Add a system
36
+ test only when focus, scroll, or multi-region behavior matters.