nitro_kit 2.0.0.alpha.5 → 2.0.0.beta.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +2 -2
  4. data/STYLE_GUIDE.md +2 -2
  5. data/app/assets/stylesheets/nitro_kit.css +298 -115
  6. data/app/components/nitro_kit/app_shell.rb +1 -1
  7. data/app/components/nitro_kit/dialog.rb +5 -3
  8. data/app/components/nitro_kit/dropzone.rb +18 -2
  9. data/app/components/nitro_kit/form_builder.rb +2 -0
  10. data/app/javascript/controllers/nk/dropzone_controller.js +20 -0
  11. data/docs/agent_guide.md +5 -0
  12. data/docs/component_contracts.md +53 -47
  13. data/docs/customization.md +3 -4
  14. data/docs/hotwire.md +2 -2
  15. data/docs/migration_1_to_2.md +1 -1
  16. data/docs/patterns/application_foundation.md +20 -2
  17. data/docs/patterns/crud_resource.md +19 -4
  18. data/docs/patterns/destructive_action.md +31 -17
  19. data/docs/patterns/inset_workspace.md +178 -0
  20. data/docs/patterns/queryable_collection.md +104 -44
  21. data/docs/patterns/resource_form.md +16 -12
  22. data/docs/rails_integration.md +1 -1
  23. data/lib/nitro_kit/version.rb +1 -1
  24. data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +6 -1
  25. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +1 -1
  26. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +10 -2
  27. data/src/stylesheets/nitro_kit/components/app_shell.css +1 -19
  28. data/src/stylesheets/nitro_kit/components/dialog.css +18 -1
  29. data/src/stylesheets/nitro_kit/components/dropzone.css +276 -72
  30. data/src/stylesheets/nitro_kit/components/pagination.css +1 -12
  31. data/src/stylesheets/nitro_kit/components/table.css +2 -1
  32. data/src/stylesheets/nitro_kit/components/toolbar.css +0 -10
  33. metadata +2 -1
@@ -12,7 +12,7 @@ Treat a 1.x migration as a product-flow review, not a helper rename. Nitro Kit
12
12
  Add the 2.0 prerelease to the application's Gemfile:
13
13
 
14
14
  ```ruby
15
- gem "nitro_kit", "2.0.0.alpha.5"
15
+ gem "nitro_kit", "2.0.0.beta.1"
16
16
  ```
17
17
 
18
18
  Bundler records the exact released version in `Gemfile.lock`; commit `Gemfile`
@@ -26,11 +26,19 @@ invited email. Existing and new users should share one acceptance path.
26
26
 
27
27
  ## Authenticated shell
28
28
 
29
- Use one `AppShell`, normally `layout: :hybrid`, for authenticated routes.
29
+ Use one `AppShell`, normally `layout: :sidebar`, for authenticated routes.
30
30
  `AppNavigation` owns brand and destinations; a `Toolbar` in `shell.topbar`
31
31
  owns the route's single `h1` and persistent actions. One wrapper inside
32
32
  `shell.main` owns responsive page padding. Do not add another viewport-height
33
- or outer-padding rule in child pages.
33
+ or outer-padding rule in child pages. The shell does not cap content width.
34
+ Tables fill the available canvas; individual form or reading pages may use a
35
+ `Container(size: :md)` or `Container(size: :lg)` inside the shared gutter.
36
+ Keep the Container intact so its width and centering stay together.
37
+
38
+ For an inset workspace, use the complete [inset composition](inset_workspace.md).
39
+ It describes the sidebar canvas and names the owner of rail
40
+ padding, canvas gaps, and page gutters. Do not reconstruct it from unrelated
41
+ spacing overrides.
34
42
 
35
43
  Application code owns destinations, authorization, and current-route policy.
36
44
  Nitro owns responsive disclosure and focus behavior. Put infrequent account
@@ -60,3 +68,13 @@ ordinary Rails flash and `303 See Other` redirects. Use the dedicated
60
68
  - Owner, administrator, and member policy differs where intended.
61
69
  - Populated, empty, invalid, narrow, settings, and destructive states work.
62
70
  - Successful mutations redirect with `303`; invalid forms render with `422`.
71
+
72
+ ## Account menu
73
+
74
+ Use a Dropdown for the signed-in identity across application examples. Include
75
+ Account and Settings links, a separator, and Sign out. Put it in the navigation
76
+ footer or existing topbar account position, consistently within each application.
77
+ Use the normal button treatment, an Avatar with photo or initials fallback, and a disclosure chevron. Route
78
+ URLs and sign-out behavior belong to the application; a real sign-out must
79
+ submit to the session endpoint using its non-GET method. The gallery's sign-out
80
+ item is an inert demonstration because it has no authenticated session.
@@ -16,18 +16,29 @@ resource with Nitro Kit.
16
16
 
17
17
  ## Resource map
18
18
 
19
- Use `AppShell(layout: :hybrid)` for an authenticated product area. Put the
19
+ Use `AppShell(layout: :sidebar)` for an authenticated product area. Put the
20
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.
21
+ place one compact Back link before the title. One layout element inside `shell.main`
22
+ owns page padding and vertical spacing; child pages add no outer gutter. For an inset treatment,
23
+ use [Inset workspace](inset_workspace.md) rather than adding padding to each
24
+ shell region. The shell owns gutters, not a universal maximum width. Let
25
+ indexes and tables fill the canvas; bound form or reading content locally with
26
+ a centered `Container(size: :md)` or `Container(size: :lg)` without extra padding.
23
27
 
24
28
  | Route | Composition |
25
29
  | --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
26
30
  | 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. |
31
+ | New/Edit | One single-column `Fieldset` and one shared form component. A toolbar submit targets the form's stable `form:` ID. |
28
32
  | Show | Status or metadata, then the resource. Keep lifecycle actions in the normal detail flow. |
29
33
  | Edit destructive area | One `DangerZone` with a safe escape. Do not put permanent deletion on every show page. |
30
34
 
35
+ Use ordinary GET forms and links with Turbo Drive for a full-page index. Keep
36
+ default caching; reserve Frames for independently navigable page regions.
37
+
38
+ Keep all columns and View/Edit actions intact at 390px. Use Table's built-in
39
+ horizontal scroll wrapper as shown in [Queryable collection](queryable_collection.md).
40
+ Do not hide columns or stack row actions to squeeze the table into the viewport.
41
+
31
42
  Use one primary action. Do not render the same Save or Create action in both
32
43
  the toolbar and form body. Use Card only for a bounded object that benefits
33
44
  from its own surface.
@@ -60,3 +71,7 @@ pagination, `303` redirects, and `422` validation. Protect the high-level
60
71
  composition: one title, one primary action, the correct form association,
61
72
  Table or EmptyState, and edit-owned destructive confirmation. Inspect
62
73
  populated, empty, invalid, narrow, draft, published, and destructive states.
74
+
75
+ Avoid layout-only wrappers inside Toolbar.leading or Toolbar.trailing: these
76
+ regions already arrange their children. Render independent action Buttons
77
+ directly; use ButtonGroup only when the actions are intentionally a joined set.
@@ -5,8 +5,9 @@ archive, or similarly destructive Rails actions.
5
5
 
6
6
  ## Summary
7
7
 
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.
8
+ - Use `NitroKit::Dialog` for destructive confirmations, including simple
9
+ deletion, member removal, and invitation revocation. A short consequence
10
+ still belongs in the application dialog, not a native browser confirm.
10
11
  - A real Rails form owns the request, and the server owns authorization.
11
12
  - Put permanent deletion on the edit route, not the operational show route.
12
13
  - Use a server-rendered review route when confirmation must work without
@@ -14,13 +15,16 @@ archive, or similarly destructive Rails actions.
14
15
 
15
16
  ## Choose one confirmation path
16
17
 
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 |
18
+ Use a Dialog with a clear title, a short consequence, a Cancel control, and a
19
+ real Rails form with a specifically named destructive submit. Use stable,
20
+ record-specific dialog IDs when rendering repeated actions in a table.
21
+ Put permanent deletion in an edit-page `DangerZone`; invitation revocation
22
+ may open a compact Dialog from its table row without adding a DangerZone.
22
23
 
23
- Never stack `turbo_confirm` inside a Dialog.
24
+ Use an ordinary link to a server-rendered review page when confirmation must
25
+ work without client JavaScript. Do not add `turbo_confirm` to a Dialog form:
26
+ it would ask twice. Native browser confirmations are not the default Nitro Kit
27
+ experience; keep them only when the host application explicitly requires them.
24
28
 
25
29
  ```ruby
26
30
  render NitroKit::DangerZone.new(
@@ -32,18 +36,23 @@ render NitroKit::DangerZone.new(
32
36
  render NitroKit::Dialog.new(id: dom_id(project, :delete_dialog)) do |dialog|
33
37
  dialog.trigger("Review deletion", variant: :destructive)
34
38
  dialog.panel(title: "Delete #{project.name}?") do
35
- form_with(
36
- model: project,
37
- method: :delete,
38
- data: { turbo_frame: "_top" }
39
- ) do
39
+ render NitroKit::Flex.new(dir: :row, gap: 2, justify: :end, wrap: :wrap) do
40
40
  render NitroKit::Button.new(
41
- "Delete project",
42
- type: :submit,
43
- variant: :destructive
41
+ "Cancel",
42
+ html: { command: "close", commandfor: "#{dialog.id}-panel" }
44
43
  )
44
+ form_with(
45
+ model: project,
46
+ method: :delete,
47
+ data: { turbo_frame: "_top" }
48
+ ) do
49
+ render NitroKit::Button.new(
50
+ "Delete project",
51
+ type: :submit,
52
+ variant: :destructive
53
+ )
54
+ end
45
55
  end
46
- dialog.close_button(label: "Cancel deletion")
47
56
  end
48
57
  end
49
58
  end
@@ -51,6 +60,11 @@ render NitroKit::DangerZone.new(
51
60
  end
52
61
  ```
53
62
 
63
+ Keep Cancel and the destructive action together in this right-aligned row.
64
+ `dialog.close_button` configures the corner X and its accessible label; it does
65
+ not render a visible footer Cancel button. The ordinary Cancel Button uses
66
+ native commands with Nitro's existing browser fallback.
67
+
54
68
  The `_top` target keeps the redirect out of a surrounding frame. The dialog is
55
69
  not a security boundary; load and authorize the record on the server.
56
70
 
@@ -0,0 +1,178 @@
1
+ # Inset workspace
2
+
3
+ **Audience:** Applications composing an inset sidebar workspace.
4
+
5
+ ## Summary
6
+
7
+ - Use the same balanced inset for sidebar layouts.
8
+ - Let navigation own rail padding, the shell own inset geometry, and one
9
+ content wrapper own page gutters.
10
+ - Join the sidebar toolbar and main region into one surface; return to
11
+ edge-to-edge content and the native navigation drawer on mobile.
12
+
13
+ The sidebar layout places navigation in a left rail and the route toolbar in
14
+ `shell.topbar`, joined to the content canvas. The other layout, `topbar`, puts
15
+ navigation above the content and the page toolbar inside `shell.main`.
16
+
17
+ ## One owner for each spacing decision
18
+
19
+ | Region | Owner | Default |
20
+ | --------------------------------------- | ------------------------------- | ------------------------------------------------------------------------- |
21
+ | Navigation rail padding | `AppNavigation` | Keep its built-in padding; no additional rail wrapper |
22
+ | Canvas gap from rail and viewport edges | `AppShell` composition | Three space units at the canvas top, right, and bottom; no extra rail gap |
23
+ | Page gutter | One `workspace-content` wrapper | Six space units on desktop, four on mobile |
24
+ | Space between fields or sections | The local `Flex`/`Grid` | No second page gutter |
25
+
26
+ With the default space token these are a 12px inset, 24px desktop gutter, and
27
+ 16px mobile gutter. The topbar uses the same 24px padding on all four sides. The rail uses only its built-in 12px padding, with no extra shell padding
28
+ on the left or gap on the right. On mobile, hide the brand and place
29
+ the title after the navigation button, with the action at the far right. Do not
30
+ add padding to the shell sidebar, an outer page container, and the page itself.
31
+
32
+ The content wrapper fills the canvas and owns only its gutter. Let tables use
33
+ that width. Bound individual forms or reading regions with a centered
34
+ `Container(size: :md)` or `Container(size: :lg)` inside the gutter; do not add
35
+ a shared shell maximum width.
36
+
37
+ ## Compose the frame
38
+
39
+ Use application-owned `data-ui` hooks. This is ordinary application CSS, not a
40
+ new component option or a copied Nitro component. For a sidebar screen:
41
+
42
+ ```ruby
43
+ AppShell(id: "workspace", layout: :sidebar, data: { ui: "inset-workspace" }) do |shell|
44
+ shell.brand { strong { "Studio" } }
45
+ shell.navigation do
46
+ AppNavigation(label: "Main navigation") do |navigation|
47
+ navigation.body do
48
+ navigation.item("Products", href: products_path, current: true)
49
+ navigation.spacer
50
+ navigation.item("Settings", href: settings_path)
51
+ end
52
+ end
53
+ end
54
+ shell.topbar do
55
+ Toolbar do |toolbar|
56
+ toolbar.leading { h1 { "Products" } }
57
+ toolbar.trailing { Button("New product", href: new_product_path, variant: :primary) }
58
+ end
59
+ end
60
+ shell.main do
61
+ div(data: { ui: "workspace-content" }) { render ProductsIndex.new(products:) }
62
+ end
63
+ end
64
+ ```
65
+
66
+ For `topbar`, put that Toolbar first inside `workspace-content` and omit
67
+ `shell.topbar`. Keep one route title and one set of actions. The header and
68
+ body in `sidebar` form one continuous canvas, not two stacked cards.
69
+
70
+ Load this stylesheet after Nitro Kit:
71
+
72
+ ```css
73
+ /* Application composition: navigation owns its rail padding, this shell owns
74
+ the inset, and workspace-content owns the only page gutter. */
75
+ :where([data-ui="workspace-content"]) {
76
+ padding: calc(var(--nk-space) * 4);
77
+ }
78
+ @media (width >= 48rem) {
79
+ :where([data-ui="inset-workspace"]) {
80
+ --workspace-inset: calc(var(--nk-space) * 3);
81
+ --nk-app-shell-background: color-mix(
82
+ in oklab,
83
+ var(--nk-color-muted) 15%,
84
+ var(--nk-color-canvas)
85
+ );
86
+ --nk-app-shell-sidebar-background: transparent;
87
+ block-size: 100vh;
88
+ block-size: 100dvh;
89
+ padding: var(--workspace-inset) var(--workspace-inset) var(
90
+ --workspace-inset
91
+ ) 0;
92
+ grid-template-rows: auto minmax(0, 1fr);
93
+ column-gap: 0;
94
+ overflow: hidden;
95
+ }
96
+ :where([data-ui="inset-workspace"] > [data-slot="app-shell-sidebar"]) {
97
+ position: static;
98
+ block-size: 100%;
99
+ border: 0;
100
+ }
101
+ :where(
102
+ [data-ui="inset-workspace"]
103
+ > [data-slot="app-shell-header"]
104
+ > [data-slot="app-shell-brand"]
105
+ ) {
106
+ padding-inline: calc(var(--nk-space) * 6);
107
+ border: 0;
108
+ }
109
+ :where([data-ui="inset-workspace"] > [data-slot="app-shell-main"]) {
110
+ overflow: auto;
111
+ overscroll-behavior: contain;
112
+ background: var(--nk-color-surface);
113
+ border: var(--nk-border-width) solid var(--nk-color-border);
114
+ border-radius: var(--nk-radius-xl);
115
+ box-shadow: var(--nk-shadow-sm);
116
+ }
117
+ :where(
118
+ [data-ui="inset-workspace"][data-layout="sidebar"]
119
+ > [data-slot="app-shell-header"]
120
+ > [data-slot="app-shell-topbar"]
121
+ ) {
122
+ padding: calc(var(--nk-space) * 6);
123
+ background: var(--nk-color-surface);
124
+ border: var(--nk-border-width) solid var(--nk-color-border);
125
+ border-block-end: 0;
126
+ border-radius: var(--nk-radius-xl) var(--nk-radius-xl) 0 0;
127
+ }
128
+ :where(
129
+ [data-ui="inset-workspace"][data-layout="sidebar"]
130
+ > [data-slot="app-shell-main"]
131
+ ) {
132
+ border-block-start: 0;
133
+ border-start-start-radius: 0;
134
+ border-start-end-radius: 0;
135
+ }
136
+ :where([data-ui="workspace-content"]) {
137
+ padding: calc(var(--nk-space) * 6);
138
+ }
139
+ }
140
+
141
+ @media (width < 48rem) {
142
+ :where(
143
+ [data-ui="inset-workspace"]
144
+ > [data-slot="app-shell-header"]
145
+ > [data-slot="app-shell-brand"]
146
+ ) {
147
+ display: none;
148
+ }
149
+ :where(
150
+ [data-ui="inset-workspace"]
151
+ [data-slot="app-shell-topbar"]
152
+ > [data-nk="toolbar"]
153
+ ) {
154
+ flex-direction: row;
155
+ flex-wrap: nowrap;
156
+ align-items: center;
157
+ }
158
+ :where([data-ui="inset-workspace"] [data-slot="toolbar-trailing"]) {
159
+ margin-inline-start: auto;
160
+ justify-content: flex-end;
161
+ flex-shrink: 0;
162
+ }
163
+ }
164
+ ```
165
+
166
+ Navigation, mobile disclosure, and focus restoration remain Nitro-owned.
167
+ Application code owns the destinations and the composition. The public
168
+ Product resource gallery runs the sidebar example with this stylesheet at
169
+ `test/dummy/app/assets/stylesheets/inset_workspace.css`.
170
+
171
+ ## Verify the result
172
+
173
+ At desktop width, check all four exposed gaps, one continuous rounded canvas,
174
+ independent main scrolling, and no rail divider stranded in the inset. In dark
175
+ mode the main surface must come forward from the quieter frame. At 390px,
176
+ verify edge-to-edge content, no document overflow, visible title/actions, and
177
+ a working navigation drawer. Include long route and navigation labels and
178
+ both empty and long content; do not judge only a short empty screen.
@@ -1,18 +1,18 @@
1
1
  # Queryable collection
2
2
 
3
3
  **Audience:** Coding agents and developers implementing filters, sorting, and
4
- pagination with Turbo Frames.
4
+ pagination with ordinary Rails GET requests and Turbo Drive.
5
5
 
6
6
  ## Summary
7
7
 
8
- - One GET-driven Turbo Frame owns filters, sorting, results, and pagination;
9
- URL parameters are the state.
8
+ - Default to ordinary GET forms and links with Turbo Drive for a full-page
9
+ collection. URL parameters are the state; keep Turbo's default caching.
10
10
  - An application query object owns allowlists, defaults, tenant scope, and page
11
11
  bounds; `NitroKit::Table` owns no query policy.
12
12
  - Pagination advances browser history; filters, reset, and sorting replace the
13
13
  current history entry.
14
- - Every response contains the same frame, including empty results; links that
15
- leave the collection target `_top`.
14
+ - Add a Turbo Frame only when the collection is an independently navigable
15
+ region of a larger page, not merely to make pagination feel faster.
16
16
 
17
17
  ## Query contract
18
18
 
@@ -33,59 +33,119 @@ query URL generation. It may expose `records`, `filters`, `current_sort`,
33
33
  `direction`, `sort_url(key)`, `pagination`, and `summary`. Ransack is one
34
34
  possible implementation, not a Nitro dependency.
35
35
 
36
- ## Frame composition
36
+ ## Page composition
37
37
 
38
38
  ```ruby
39
- FRAME_ID = "projects-results"
40
-
41
- turbo_frame_tag(FRAME_ID, data: { turbo_action: "advance" }) do
42
- form_with(
43
- scope: :q,
44
- url: projects_path,
45
- method: :get,
46
- builder: NitroKit::FormBuilder,
47
- data: { turbo_frame: FRAME_ID, turbo_action: "replace" }
48
- ) do |form|
49
- form.group do
50
- form.field(:name_cont, as: :search, label: "Search")
51
- form.submit("Apply filters")
39
+ form_with(
40
+ scope: :q,
41
+ url: projects_path,
42
+ method: :get,
43
+ builder: NitroKit::FormBuilder,
44
+ data: { turbo_action: "replace" }
45
+ ) do |form|
46
+ form.group do
47
+ form.field(:name_cont, as: :search, label: "Search")
48
+ form.submit("Apply filters")
49
+ end
50
+ end
51
+
52
+ render NitroKit::Table.new(
53
+ sort: query.current_sort,
54
+ direction: query.direction
55
+ ) do |table|
56
+ table.caption("Projects")
57
+ table.thead do
58
+ table.tr do
59
+ table.th(:name, sort: :name, href: query.sort_url(:name),
60
+ sort_data: { turbo_action: "replace" })
52
61
  end
53
62
  end
63
+ table.tbody do
64
+ query.records.each { |project| render_project_row(table, project) }
65
+ end
66
+ end
67
+
68
+ render NitroKit::PaginationBar.new do |bar|
69
+ bar.summary(query.summary)
70
+ bar.pagination(query.pagination)
71
+ end
72
+ ```
73
+
74
+ Reset with the plain collection URL so stale parameters disappear. Sort,
75
+ filter, and reset controls may use `turbo_action: "replace"` to avoid filling
76
+ history with refinements; pagination uses ordinary links and advances history.
77
+ No result frame, frame targets, cache opt-out, or custom JavaScript is needed.
78
+ Optional autosubmit may call the same GET form's `requestSubmit`.
79
+
80
+ ## When a frame is useful
81
+
82
+ For an independent collection within a larger page, wrap the region in a
83
+ stable `turbo_frame_tag("projects-results", data: { turbo_action: "advance" })`.
84
+ Return that frame for populated and empty responses, and target `_top` on
85
+ links that should open complete pages. Exercise repeated refinements followed
86
+ by pagination and Back/Forward, checking actual rows and controls as well as
87
+ the URL. Do not disable Turbo caching just to make a flaky history test pass;
88
+ first distinguish preview/test timing from an incorrect restored snapshot.
54
89
 
55
- render NitroKit::Table.new(
56
- sort: query.current_sort,
57
- direction: query.direction
58
- ) do |table|
59
- table.caption("Projects")
60
- table.thead do
90
+ ## Keep tables intact at every viewport
91
+
92
+ Every Table owns a horizontal scroll wrapper. Keep all columns and row actions
93
+ in their desktop arrangement on mobile; scroll the table instead of hiding
94
+ columns, stacking buttons, or forcing narrow column widths. Cells preserve
95
+ unbroken labels so controls retain their intrinsic size.
96
+
97
+ This complete table uses application-owned hooks. Adapt the fields and routes,
98
+ then load the accompanying CSS after Nitro Kit:
99
+
100
+ ```ruby
101
+ Table(data: { ui: "resource-table" }, table_aria: { label: "Projects" }) do |table|
102
+ table.thead do
103
+ table.tr do
104
+ table.th("Project", data: { resource_column: "name" })
105
+ table.th("Status", data: { resource_column: "status" })
106
+ table.th("Updated", data: { resource_column: "secondary" })
107
+ table.th("Actions", data: { resource_column: "actions" })
108
+ end
109
+ end
110
+ table.tbody do
111
+ projects.each do |project|
61
112
  table.tr do
62
- table.th(:name, sort: :name, href: query.sort_url(:name),
63
- sort_data: { turbo_action: "replace" })
113
+ table.th(project.name, scope: :row, data: { resource_column: "name" })
114
+ table.td(data: { resource_column: "status" }) do
115
+ Badge(project.archived? ? "Archived" : "Active", size: :sm)
116
+ end
117
+ table.td(project.updated_at.to_date.to_fs(:long), data: { resource_column: "secondary" })
118
+ table.td(data: { resource_column: "actions" }) do
119
+ Flex(dir: :row, gap: 1, align: :stretch, justify: :end) do
120
+ Button("View", href: project_path(project), size: :sm)
121
+ Button("Edit", href: edit_project_path(project), size: :sm)
122
+ end
123
+ end
64
124
  end
65
125
  end
66
- table.tbody do
67
- query.records.each { |project| render_project_row(table, project) }
68
- end
69
- end
70
-
71
- render NitroKit::PaginationBar.new do |bar|
72
- bar.summary(query.summary)
73
- bar.pagination(query.pagination)
74
126
  end
75
127
  end
76
128
  ```
77
129
 
78
- Reset with the plain collection URL so stale parameters disappear. Sort and
79
- filter controls use `turbo_action: "replace"`; pagination inherits the frame's
80
- `advance`. Give View, Edit, and New links
81
- `data: { turbo_frame: "_top" }` or place them outside the frame.
130
+ ```css
131
+ /* Table owns horizontal scrolling; record names keep their secondary line. */
132
+ :where([data-ui="resource-table"] tbody [data-resource-column="name"] > *) {
133
+ display: block;
134
+ }
135
+ ```
82
136
 
83
- No Stimulus controller is required. Optional autosubmit may call the same GET
84
- form's `requestSubmit`; the URL remains the source of truth.
137
+ The public Product resource gallery runs this composition. Table provides the
138
+ scroll wrapper automatically; no extra overflow wrapper is needed.
85
139
 
86
140
  ## Tests
87
141
 
88
142
  Request-test parameter preservation, safe fallback for invalid sort keys, and
89
- the stable frame in populated and empty responses. System-test filter → sort →
90
- paginate → Back/Forward, address-bar changes, and a row link leaving the frame.
91
- Use Capybara waiting assertions, not sleeps.
143
+ populated and empty responses. System-test repeated filter → sort →
144
+ paginate → Back/Forward, restored controls and rows, address-bar changes, and
145
+ full-page row links. A correct URL alone does not prove restoration worked.
146
+ When a visit displays a cached preview, wait for `html[data-turbo-preview]` to
147
+ disappear before entering fields or submitting another form. Keep caching
148
+ enabled in the test.
149
+ Use Capybara waiting assertions, not sleeps. At 390px, assert that the page stays within the viewport and the table scrolls
150
+ horizontally to reveal intact row actions, including a long unbroken resource
151
+ name and a multi-word status. Repeat in light and dark appearances.
@@ -15,6 +15,12 @@ and update forms.
15
15
 
16
16
  ## Form
17
17
 
18
+ Use one full-width column of fields inside a centered `Container(size: :lg)`
19
+ for ordinary resource forms. A `Fieldset` puts its legend above the controls.
20
+ Reserve the two-column `SettingsSection` for settings pages with enough room
21
+ for both explanatory text and fields; nesting it in a narrow form container
22
+ squeezes the controls.
23
+
18
24
  ```ruby
19
25
  module UI
20
26
  class ProjectForm < Phlex::HTML
@@ -26,18 +32,16 @@ module UI
26
32
  end
27
33
 
28
34
  def view_template
29
- render NitroKit::SettingsSection.new(title: "Project details") do |section|
30
- section.form do
31
- form_with(
32
- model: @project,
33
- builder: NitroKit::FormBuilder,
34
- id: @form_id
35
- ) do |form|
36
- form.group do
37
- form.field(:name, required: true)
38
- form.field(:status, as: :select, options: Project.statuses.keys)
39
- form.field(:description, as: :textarea)
40
- end
35
+ render NitroKit::Container.new(size: :lg) do
36
+ form_with(
37
+ model: @project,
38
+ builder: NitroKit::FormBuilder,
39
+ id: @form_id
40
+ ) do |form|
41
+ form.fieldset(legend: "Project details") do
42
+ form.field(:name, required: true)
43
+ form.field(:status, as: :select, options: Project.statuses.keys)
44
+ form.field(:description, as: :textarea)
41
45
  end
42
46
  end
43
47
  end
@@ -12,7 +12,7 @@ There are no `nk_form_with` helpers or general ERB component bridge.
12
12
  Pin the current prerelease:
13
13
 
14
14
  ```ruby
15
- gem "nitro_kit", "2.0.0.alpha.5"
15
+ gem "nitro_kit", "2.0.0.beta.1"
16
16
  ```
17
17
 
18
18
  Use the released gem and commit `Gemfile` with `Gemfile.lock`. Before upgrading,
@@ -1,3 +1,3 @@
1
1
  module NitroKit
2
- VERSION = "2.0.0.alpha.5"
2
+ VERSION = "2.0.0.beta.1"
3
3
  end
@@ -58,7 +58,7 @@ Keep frames around complete resource or collection regions, not individual butto
58
58
  - Do not describe that HTML branch as a JavaScript-free interaction when its
59
59
  control still depends on Turbo or a closed overlay.
60
60
  - Let GET query parameters be the source of truth for filtering, sorting, and pagination.
61
- - Use native Nitro Dialog behavior for reviewed destructive actions. Use `data: { turbo_confirm: ... }` for compact confirmations that do not need a dialog.
61
+ - Use Nitro Dialog for destructive confirmations, including short delete, remove, and revoke flows. Put the real Rails form inside the dialog; do not add `turbo_confirm`. Follow `docs/patterns/destructive_action.md`.
62
62
  - Render flash through `NitroKit::Toast::FlashMessages`; the application owns setting the flash.
63
63
 
64
64
  ## Preserve ownership
@@ -81,3 +81,8 @@ before changing application code. Disable Chrome's background throttling or
81
81
  use a test-only `requestAnimationFrame` shim when necessary. Never ship that
82
82
  workaround in the application or replace a conventional Turbo flow to satisfy
83
83
  one browser driver.
84
+
85
+ For a full-page CRUD index, default to ordinary GET forms and links with Turbo
86
+ Drive and normal caching. Use Frames for independently navigable regions.
87
+ History tests must check restored rows and controls, not just the URL, and
88
+ wait past `html[data-turbo-preview]` before interacting with a new page.
@@ -65,7 +65,7 @@ contract as a substitute for the installed API.
65
65
  - Render HTML on the server and add Hotwire progressively.
66
66
  - Set the document language on the root `html` element.
67
67
  - Test with Minitest and fixtures, including tenancy and unhappy paths.
68
- - In authenticated admin areas, default to a hybrid `AppShell` with the route's
68
+ - In authenticated admin areas, default to a sidebar `AppShell` with the route's
69
69
  one `h1` and basic actions in its `Toolbar`. Keep one page gutter and avoid
70
70
  repeated headings or automatic Card wrappers. At narrow widths, let trailing
71
71
  actions stack below a Back affordance and title instead of clipping the title
@@ -55,9 +55,13 @@ If the gem is not installed, say that the skill requires Nitro Kit and follow th
55
55
  5. Keep routes, authorization, records, query policy, DOM IDs, Turbo boundaries, and response semantics in the application.
56
56
  6. Translate the application's semantic theme into documented `--nk-*` properties instead of choosing similar raw palette values. Use `--nk-button-radius` when Button shape intentionally differs from inputs and surfaces.
57
57
  7. Verify closed options and required compound declarations before rendering.
58
- 8. For authenticated CRUD, prefer a hybrid `AppShell` with a `Toolbar` that
58
+ 8. For authenticated CRUD, prefer a sidebar `AppShell` with a `Toolbar` that
59
59
  owns the route's single `h1` and basic actions. The shell main region owns
60
- one content gutter. Do not repeat that heading in `PageHeader`, or wrap each
60
+ one content gutter, not a universal maximum width. Tables fill the canvas;
61
+ form and reading pages may use a centered Container inside that gutter.
62
+ Simple resource forms use a single-column Fieldset in Container lg so fields
63
+ use the available width; reserve split SettingsSections for roomy settings pages.
64
+ Do not repeat that heading in `PageHeader`, or wrap each
61
65
  table, form, and detail region in another Card. At narrow widths, preserve
62
66
  the full title and persistent actions by stacking the trailing actions below
63
67
  the title rather than clipping either region.
@@ -92,3 +96,7 @@ completeness. Do not use a green Doctor result as proof that every relevant
92
96
  screen, state, or catalog workflow has been implemented.
93
97
 
94
98
  For a migration, Doctor is an inventory, not visual proof. Run representative form and component rendering with `ActiveModel::Translation.raise_on_missing_translations` enabled when the application uses strict i18n. Compare the same representative flows in a browser at wide and narrow widths, exercise keyboard focus, and inspect computed styles for missing application classes, stacked Button content, broken compound corners, double focus rings, clipping, and theme drift. Re-audit rendered native buttons, Rails button helpers, and application-owned button classes before declaring the conversion complete. Search the whole application for `desperately_need_a_class:` and review every result, aiming for zero. Move layout and visual treatment to application-owned wrappers, remove generic class forwarding, accept incidental Nitro defaults, and keep unmatched product UI application-owned; retain only documented external-integration hooks.
99
+
100
+ Use Nitro Dialog for destructive confirmations, including simple deletion,
101
+ member removal, and invitation revocation. Read `docs/patterns/destructive_action.md`;
102
+ do not substitute native browser confirmation for short messages.