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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +17 -2
- data/STYLE_GUIDE.md +2 -2
- data/app/assets/stylesheets/nitro_kit.css +298 -115
- data/app/components/nitro_kit/app_shell.rb +1 -1
- data/app/components/nitro_kit/dialog.rb +5 -3
- data/app/components/nitro_kit/dropzone.rb +18 -2
- data/app/components/nitro_kit/form_builder.rb +2 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +20 -0
- data/docs/agent_guide.md +27 -0
- data/docs/component_contracts.md +53 -47
- data/docs/customization.md +3 -4
- data/docs/hotwire.md +2 -2
- data/docs/initialization_prompt.md +12 -6
- data/docs/migration_1_to_2.md +1 -1
- data/docs/patterns/application_foundation.md +20 -2
- data/docs/patterns/crud_resource.md +19 -4
- data/docs/patterns/destructive_action.md +31 -17
- data/docs/patterns/inset_workspace.md +178 -0
- data/docs/patterns/queryable_collection.md +104 -44
- data/docs/patterns/resource_form.md +16 -12
- data/docs/rails_integration.md +1 -1
- data/lib/nitro_kit/installation.rb +8 -0
- data/lib/nitro_kit/version.rb +1 -1
- data/plugins/nitro-kit/.codex-plugin/plugin.json +4 -4
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +18 -1
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +1 -1
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +16 -1
- data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +1 -1
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +28 -5
- data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +1 -1
- data/src/stylesheets/nitro_kit/components/app_shell.css +1 -19
- data/src/stylesheets/nitro_kit/components/dialog.css +18 -1
- data/src/stylesheets/nitro_kit/components/dropzone.css +276 -72
- data/src/stylesheets/nitro_kit/components/pagination.css +1 -12
- data/src/stylesheets/nitro_kit/components/table.css +2 -1
- data/src/stylesheets/nitro_kit/components/toolbar.css +0 -10
- metadata +2 -1
data/docs/customization.md
CHANGED
|
@@ -426,12 +426,11 @@ module Workspace
|
|
|
426
426
|
end
|
|
427
427
|
```
|
|
428
428
|
|
|
429
|
-
Change only `layout:` to `:topbar` or `:
|
|
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
|
|
44
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
+
7. Verify one application base component includes `NitroKit`, and model-backed
|
|
20
25
|
forms select `NitroKit::FormBuilder` explicitly.
|
|
21
|
-
|
|
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
|
data/docs/migration_1_to_2.md
CHANGED
|
@@ -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.
|
|
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: :
|
|
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: :
|
|
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
|
|
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 `
|
|
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`
|
|
9
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
42
|
-
|
|
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
|
|
4
|
+
pagination with ordinary Rails GET requests and Turbo Drive.
|
|
5
5
|
|
|
6
6
|
## Summary
|
|
7
7
|
|
|
8
|
-
-
|
|
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
|
-
-
|
|
15
|
-
|
|
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
|
-
##
|
|
36
|
+
## Page composition
|
|
37
37
|
|
|
38
38
|
```ruby
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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(
|
|
63
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
paginate → Back/Forward, address-bar changes, and
|
|
91
|
-
|
|
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::
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
) do
|
|
36
|
-
form.
|
|
37
|
-
|
|
38
|
-
|
|
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
|
data/docs/rails_integration.md
CHANGED
|
@@ -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.
|
|
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,
|