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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +2 -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 +5 -0
- data/docs/component_contracts.md +53 -47
- data/docs/customization.md +3 -4
- data/docs/hotwire.md +2 -2
- 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/version.rb +1 -1
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +6 -1
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +1 -1
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +10 -2
- 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/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.
|
|
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: :
|
|
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.
|
|
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,
|
data/lib/nitro_kit/version.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|