nitro_kit 2.0.0.alpha.4 → 2.0.0.alpha.6

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/README.md +17 -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 +27 -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/initialization_prompt.md +12 -6
  16. data/docs/migration_1_to_2.md +1 -1
  17. data/docs/patterns/application_foundation.md +20 -2
  18. data/docs/patterns/crud_resource.md +19 -4
  19. data/docs/patterns/destructive_action.md +31 -17
  20. data/docs/patterns/inset_workspace.md +178 -0
  21. data/docs/patterns/queryable_collection.md +104 -44
  22. data/docs/patterns/resource_form.md +16 -12
  23. data/docs/rails_integration.md +1 -1
  24. data/lib/nitro_kit/installation.rb +8 -0
  25. data/lib/nitro_kit/version.rb +1 -1
  26. data/plugins/nitro-kit/.codex-plugin/plugin.json +4 -4
  27. data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +18 -1
  28. data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +1 -1
  29. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +16 -1
  30. data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +1 -1
  31. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +28 -5
  32. data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +1 -1
  33. data/src/stylesheets/nitro_kit/components/app_shell.css +1 -19
  34. data/src/stylesheets/nitro_kit/components/dialog.css +18 -1
  35. data/src/stylesheets/nitro_kit/components/dropzone.css +276 -72
  36. data/src/stylesheets/nitro_kit/components/pagination.css +1 -12
  37. data/src/stylesheets/nitro_kit/components/table.css +2 -1
  38. data/src/stylesheets/nitro_kit/components/toolbar.css +0 -10
  39. metadata +2 -1
@@ -426,12 +426,11 @@ module Workspace
426
426
  end
427
427
  ```
428
428
 
429
- Change only `layout:` to `:topbar` or `:hybrid`; the same `brand`, `navigation`, `topbar`, and `main` declarations remain valid. Nitro owns the responsive breakpoint, narrow drawer, focus management, sticky regions, and one reflowed navigation DOM tree. Do not clone navigation for mobile or add route registries to the shell.
429
+ Change only `layout:` to `:topbar` or `:sidebar`; the same `brand`, `navigation`, `topbar`, and `main` declarations remain valid. Nitro owns the responsive breakpoint, narrow drawer, focus management, sticky regions, and one reflowed navigation DOM tree. Do not clone navigation for mobile or add route registries to the shell.
430
430
 
431
431
  The gallery has executable examples for
432
- [sidebar](https://gallery.nitrokit.dev/gallery/compositions/application-sidebar),
433
- [topbar](https://gallery.nitrokit.dev/gallery/compositions/application-topbar),
434
- and [hybrid](https://gallery.nitrokit.dev/gallery/compositions/application-hybrid)
432
+ [sidebar](https://gallery.nitrokit.dev/gallery/compositions/application-sidebar) and
433
+ [topbar](https://gallery.nitrokit.dev/gallery/compositions/application-topbar)
435
434
  applications.
436
435
 
437
436
  ## Rails forms and Hotwire
data/docs/hotwire.md CHANGED
@@ -40,8 +40,8 @@ interaction available.
40
40
  ## Stimulus and lifecycle
41
41
 
42
42
  Let Turbo submit real Rails forms. Use `data-turbo-submits-with` for submission
43
- feedback and `data-turbo-confirm` only for compact confirmation. A reviewed
44
- destructive flow still submits a real Rails form; use the
43
+ feedback. Destructive confirmations use Nitro Dialog, including compact
44
+ remove and revoke flows, with a real Rails form inside; use the
45
45
  [destructive action pattern](patterns/destructive_action.md).
46
46
 
47
47
  Keep application controllers declarative. Prefer `data-action` over manually
@@ -6,20 +6,26 @@
6
6
  `2.`.
7
7
  2. Choose the project-local Nitro Kit skill matching the task. It will resolve
8
8
  and read the installed, version-matched `docs/agent_guide.md`.
9
- 3. Inspect the application before editing. Preserve established view, asset,
9
+ 3. For greenfield planning or broad product work, check whether Nitro Kit
10
+ catalog or MCP tools are available. When available, inventory and search by
11
+ product workflow, retrieve relevant patterns, and state what will be used,
12
+ adapted, or deferred. When unavailable, continue with the bundled guidance;
13
+ catalog access is optional and must never block the work.
14
+ 4. Inspect the application before editing. Preserve established view, asset,
10
15
  authentication, and testing conventions unless the task changes them.
11
- 4. For a greenfield application, run `bin/rails generate phlex:install` and use
16
+ 5. For a greenfield application, run `bin/rails generate phlex:install` and use
12
17
  Phlex for the application layout, route views, and reusable UI. In an
13
18
  established application, introduce Phlex only at the requested boundary.
14
19
  Do not perform an application-wide migration unless it is explicitly
15
20
  authorized.
16
- 5. Verify that the application loads Nitro Kit CSS, the appearance bootstrap,
21
+ 6. Verify that the application loads Nitro Kit CSS, the appearance bootstrap,
17
22
  Turbo, Stimulus, and the normal Stimulus controller loader. Never copy Nitro
18
23
  components or `nk--*` controllers into the application.
19
- 6. Verify one application base component includes `NitroKit`, and model-backed
24
+ 7. Verify one application base component includes `NitroKit`, and model-backed
20
25
  forms select `NitroKit::FormBuilder` explicitly.
21
- 7. Run `bin/rails nitro_kit:doctor`, fix actionable failures, and run the
22
- application's relevant tests.
26
+ 8. Run `bin/rails nitro_kit:doctor`, fix actionable failures, and run the
27
+ application's relevant tests. Doctor verifies Nitro Kit integration, not
28
+ whether the product implements every relevant workflow.
23
29
 
24
30
  If this is a Nitro Kit 1.x migration, stop and follow
25
31
  `docs/migration_1_to_2.md` from the installed gem. Replace a control only when
@@ -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.4"
15
+ gem "nitro_kit", "2.0.0.alpha.6"
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.4"
15
+ gem "nitro_kit", "2.0.0.alpha.6"
16
16
  ```
17
17
 
18
18
  Use the released gem and commit `Gemfile` with `Gemfile.lock`. Before upgrading,