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
|
@@ -37,7 +37,7 @@ module NitroKit
|
|
|
37
37
|
# `:input` keeps the native file input visible beside the drop target.
|
|
38
38
|
# `:minimal` leaves the drop target as the only visible affordance; the
|
|
39
39
|
# input stays in the accessibility tree and keeps its own focus ring.
|
|
40
|
-
PRESENTATIONS = %i[input minimal].freeze
|
|
40
|
+
PRESENTATIONS = %i[input minimal avatar compact].freeze
|
|
41
41
|
|
|
42
42
|
def initialize(
|
|
43
43
|
id:,
|
|
@@ -45,6 +45,7 @@ module NitroKit
|
|
|
45
45
|
label: I18n.t("nitro_kit.dropzone.label"),
|
|
46
46
|
description: nil,
|
|
47
47
|
presentation: :minimal,
|
|
48
|
+
inline: false,
|
|
48
49
|
direct_upload: true,
|
|
49
50
|
multiple: false,
|
|
50
51
|
accept: nil,
|
|
@@ -62,6 +63,7 @@ module NitroKit
|
|
|
62
63
|
@label = required_text(:label, label)
|
|
63
64
|
@description = optional_text(:description, description)
|
|
64
65
|
@presentation = validate_choice!(:presentation, presentation, PRESENTATIONS)
|
|
66
|
+
@inline = validate_boolean!(:inline, inline)
|
|
65
67
|
@direct_upload = validate_boolean!(:direct_upload, direct_upload)
|
|
66
68
|
@multiple = validate_boolean!(:multiple, multiple)
|
|
67
69
|
@accept = optional_text(:accept, accept)
|
|
@@ -70,6 +72,10 @@ module NitroKit
|
|
|
70
72
|
@disabled = validate_boolean!(:disabled, disabled)
|
|
71
73
|
@required = validate_boolean!(:required, required)
|
|
72
74
|
|
|
75
|
+
if @presentation == :avatar && (@multiple || @inline)
|
|
76
|
+
raise ArgumentError, "avatar presentation requires a single file and inline: false"
|
|
77
|
+
end
|
|
78
|
+
|
|
73
79
|
if !@multiple && @max_files != 1
|
|
74
80
|
raise ArgumentError, "max_files must be 1 when multiple is false"
|
|
75
81
|
end
|
|
@@ -81,6 +87,7 @@ module NitroKit
|
|
|
81
87
|
aria: { disabled: @disabled ? true : nil },
|
|
82
88
|
data: {
|
|
83
89
|
presentation: @presentation,
|
|
90
|
+
layout: @inline ? "inline" : "stacked",
|
|
84
91
|
controller: @disabled ? nil : "nk--dropzone",
|
|
85
92
|
state: @disabled ? "disabled" : "idle",
|
|
86
93
|
action: @disabled ? nil : dropzone_actions,
|
|
@@ -125,6 +132,13 @@ module NitroKit
|
|
|
125
132
|
attributes: { for: input_id }
|
|
126
133
|
)
|
|
127
134
|
) do
|
|
135
|
+
if @presentation == :avatar
|
|
136
|
+
img(**slot_attributes(:avatar_image, attributes: { alt: "", hidden: true }))
|
|
137
|
+
span(**slot_attributes(:icon)) { render Icon.new(:user_round, size: :lg) }
|
|
138
|
+
span(**slot_attributes(:upload_badge)) { render Icon.new(:arrow_up, size: :sm) }
|
|
139
|
+
elsif @presentation != :compact
|
|
140
|
+
span(**slot_attributes(:icon)) { render Icon.new(:cloud_upload, size: :lg) }
|
|
141
|
+
end
|
|
128
142
|
strong(**slot_attributes(:title, attributes: { id: title_id })) { plain(@label) }
|
|
129
143
|
span(**slot_attributes(:instruction)) { plain(I18n.t("nitro_kit.dropzone.prompt")) }
|
|
130
144
|
span(**slot_attributes(:compact_instruction)) { plain(I18n.t("nitro_kit.dropzone.compact_prompt")) }
|
|
@@ -216,6 +230,7 @@ module NitroKit
|
|
|
216
230
|
)
|
|
217
231
|
) do
|
|
218
232
|
li(**slot_attributes(:preview, attributes: { data: { state: "queued" } })) do
|
|
233
|
+
span(**slot_attributes(:file_icon)) { render Icon.new(:file, size: :md) }
|
|
219
234
|
img(**slot_attributes(:preview_image, attributes: { alt: "", hidden: true }))
|
|
220
235
|
|
|
221
236
|
span(**slot_attributes(:file)) do
|
|
@@ -262,7 +277,8 @@ module NitroKit
|
|
|
262
277
|
"dragover->nk--dropzone#dragOver",
|
|
263
278
|
"dragleave->nk--dropzone#dragLeave",
|
|
264
279
|
"drop->nk--dropzone#drop",
|
|
265
|
-
"submit@document->nk--dropzone#submit",
|
|
280
|
+
"submit@document->nk--dropzone#submit:capture",
|
|
281
|
+
"turbo:submit-end@document->nk--dropzone#submitted",
|
|
266
282
|
"turbo:before-cache@document->nk--dropzone#teardown"
|
|
267
283
|
].join(" ")
|
|
268
284
|
end
|
|
@@ -116,6 +116,7 @@ module NitroKit
|
|
|
116
116
|
label: I18n.t("nitro_kit.dropzone.label"),
|
|
117
117
|
description: nil,
|
|
118
118
|
presentation: :minimal,
|
|
119
|
+
inline: false,
|
|
119
120
|
direct_upload: true,
|
|
120
121
|
multiple: false,
|
|
121
122
|
accept: nil,
|
|
@@ -137,6 +138,7 @@ module NitroKit
|
|
|
137
138
|
label:,
|
|
138
139
|
description:,
|
|
139
140
|
presentation:,
|
|
141
|
+
inline:,
|
|
140
142
|
direct_upload:,
|
|
141
143
|
multiple:,
|
|
142
144
|
accept:,
|
|
@@ -181,6 +181,12 @@ export default class extends Controller {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
submitted(event) {
|
|
185
|
+
if (event.target === this.element.closest("form")) {
|
|
186
|
+
this.inputTarget.disabled = false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
184
190
|
teardown() {
|
|
185
191
|
this.release({ clearInput: true });
|
|
186
192
|
}
|
|
@@ -203,6 +209,7 @@ export default class extends Controller {
|
|
|
203
209
|
this.releaseEntry(entry, { removeElement: true }),
|
|
204
210
|
);
|
|
205
211
|
this.entries = [];
|
|
212
|
+
this.updateAvatar();
|
|
206
213
|
this.restoreSubmitControls();
|
|
207
214
|
this.dragDepth = 0;
|
|
208
215
|
|
|
@@ -394,7 +401,20 @@ export default class extends Controller {
|
|
|
394
401
|
this.previewListTarget.hidden = this.entries.length === 0;
|
|
395
402
|
}
|
|
396
403
|
|
|
404
|
+
updateAvatar() {
|
|
405
|
+
const image = this.element.querySelector(
|
|
406
|
+
'[data-slot="dropzone-avatar-image"]',
|
|
407
|
+
);
|
|
408
|
+
if (!image) return;
|
|
409
|
+
|
|
410
|
+
const url = this.entries[0]?.objectUrl;
|
|
411
|
+
image.hidden = !url;
|
|
412
|
+
if (url) image.src = url;
|
|
413
|
+
else image.removeAttribute("src");
|
|
414
|
+
}
|
|
415
|
+
|
|
397
416
|
reflectState() {
|
|
417
|
+
this.updateAvatar();
|
|
398
418
|
if (this.dragDepth > 0) return;
|
|
399
419
|
|
|
400
420
|
if (
|
data/docs/agent_guide.md
CHANGED
|
@@ -19,6 +19,7 @@ Read this guide from that directory. The installed
|
|
|
19
19
|
| -------------------------------------------------- | ------------------------------------------------------------ |
|
|
20
20
|
| Rails models, routes, CRUD, and tests | [Rails conventions](rails_conventions.md) |
|
|
21
21
|
| Authentication, teams, shell, and settings | [Application foundation](patterns/application_foundation.md) |
|
|
22
|
+
| Inset canvas and shell spacing | [Inset workspace](patterns/inset_workspace.md) |
|
|
22
23
|
| Complete product CRUD | [CRUD resource](patterns/crud_resource.md) |
|
|
23
24
|
| Components and composition | [Component contracts](component_contracts.md) |
|
|
24
25
|
| Installation, assets, forms, and Rails integration | [Rails integration](rails_integration.md) |
|
|
@@ -35,6 +36,28 @@ Read this guide from that directory. The installed
|
|
|
35
36
|
Before changing a Nitro Kit 1.x application, read the migration guide. Do not
|
|
36
37
|
infer 2.x APIs from memory or old application code.
|
|
37
38
|
|
|
39
|
+
## Discover optional product patterns
|
|
40
|
+
|
|
41
|
+
The installed skills, documentation, component contracts, and source are the
|
|
42
|
+
complete required path for using Nitro Kit. A Nitro Kit catalog may also be
|
|
43
|
+
available through MCP or another tool, but catalog access is optional and must
|
|
44
|
+
never block the work.
|
|
45
|
+
|
|
46
|
+
For greenfield planning or broad product work:
|
|
47
|
+
|
|
48
|
+
1. Check whether Nitro Kit catalog or MCP tools are available.
|
|
49
|
+
2. When available, inventory the catalog, then search by product workflow rather
|
|
50
|
+
than component name.
|
|
51
|
+
3. Retrieve the relevant patterns before implementation and record which will be
|
|
52
|
+
used, adapted, or deferred.
|
|
53
|
+
4. When unavailable, continue with the installed guidance and report no catalog
|
|
54
|
+
coverage claims.
|
|
55
|
+
|
|
56
|
+
For a focused component or interaction change, search only when a catalog tool
|
|
57
|
+
is already available and the task could benefit from a higher-level pattern.
|
|
58
|
+
`nitro_kit:doctor` validates installation and runtime contracts; it is not a
|
|
59
|
+
product-completeness audit.
|
|
60
|
+
|
|
38
61
|
## Preserve the application's architecture
|
|
39
62
|
|
|
40
63
|
For a greenfield application, run:
|
|
@@ -58,6 +81,10 @@ explicitly authorized.
|
|
|
58
81
|
- Put stacked fields and actions in `form.group` or `FieldGroup`.
|
|
59
82
|
- Use documented component options, compound methods, native attributes, and
|
|
60
83
|
public `--nk-*` tokens.
|
|
84
|
+
- Default to Nitro `Dialog` for destructive confirmations, even short member
|
|
85
|
+
removal or invitation revocation. See the destructive-action pattern.
|
|
86
|
+
- Let the shell own gutters and each page choose content width: full width for
|
|
87
|
+
tables, a centered Container for bounded forms or reading content.
|
|
61
88
|
- Keep product policy, records, routes, authorization, queries, DOM IDs, and
|
|
62
89
|
server responses in application code.
|
|
63
90
|
- Do not copy Nitro source, add `nk_*` helpers, mutate Nitro controllers, or
|
data/docs/component_contracts.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
API. This table is also parsed by the gallery; keep every row to four cells and
|
|
5
5
|
escape literal pipe characters.
|
|
6
6
|
|
|
7
|
-
This is the shipped public catalog for `2.0.0.alpha.
|
|
7
|
+
This is the shipped public catalog for `2.0.0.alpha.6`. It records current Ruby
|
|
8
8
|
construction, rendered roots, closed vocabularies, compound cardinalities, and
|
|
9
9
|
integration boundaries. For task guidance, start with the
|
|
10
10
|
[agent guide](agent_guide.md) or [Rails integration](rails_integration.md).
|
|
@@ -60,24 +60,24 @@ Every default string a person can read or hear comes from the engine-loaded `nit
|
|
|
60
60
|
|
|
61
61
|
### Forms
|
|
62
62
|
|
|
63
|
-
| Component | Constructor-specific options
|
|
64
|
-
| ------------------ |
|
|
65
|
-
| `AppearancePicker` | required `id:`; `label:` defaulting to `I18n.t("nitro_kit.appearance_picker.label")`; `label_visible: true`; `presentation: :segmented`; `preference: :system`
|
|
66
|
-
| `Checkbox` | `label: nil`, `description: nil`, `id: nil`, `name: nil`, `value: "1"`, `unchecked_value: "0"`, `include_hidden: true`, `checked: false`, `indeterminate: false`, `disabled: false`, `required: false`, `invalid: false`, `size: :md`, `control_html: {}`, `control_aria: {}`, `control_data: {}`
|
|
67
|
-
| `CheckboxGroup` | required `legend:`, `options:`, `name:`; `value: []`, `id: nil`, `description: nil`, `orientation: :vertical`, `presentation: :list`, `unchecked_value: ""`, `include_hidden: true`, `disabled: false`, `required: false`, `size: :md`
|
|
68
|
-
| `ControlGroup` | `label: nil`, `id: nil`
|
|
69
|
-
| `Dropzone` | required `id:`, `name:`; `label:` defaulting to `I18n.t("nitro_kit.dropzone.label")`, `description: nil`, `presentation: :minimal` (`minimal input`), `direct_upload: true`, `multiple: false`, `accept: nil`, `max_files: 1`, `max_bytes: nil`, `disabled: false`, `required: false`
|
|
70
|
-
| `Field` | optional Rails form builder and field name; see the full signature below
|
|
71
|
-
| `RichTextArea` | required captured editor content as an `ActiveSupport::SafeBuffer`; `id: nil`
|
|
72
|
-
| `FieldGroup` | no component-specific keywords
|
|
73
|
-
| `Fieldset` | `legend: nil`, `description: nil`, `disabled: false`, `name: nil`
|
|
74
|
-
| `Input` | `type: :text`, `id: nil`, `name: nil`, `value: nil`, `placeholder: nil`, `disabled: false`, `readonly: false`, `required: false`, `autocomplete: nil`, `min: nil`, `max: nil`, `step: nil`, `minlength: nil`, `maxlength: nil`, `multiple: false`, `accept: nil`, `pattern: nil`, `inputmode: nil`, `checked: nil`
|
|
75
|
-
| `Label` | optional text; `for: nil`, `id: nil`
|
|
76
|
-
| `RadioButton` | `label: nil`, `description: nil`, `id: nil`, `name: nil`, `value: "1"`, `checked: false`, `disabled: false`, `required: false`, `invalid: false`, `size: :md`, `control_html: {}`, `control_aria: {}`, `control_data: {}`
|
|
77
|
-
| `RadioButtonGroup` | required `legend:`, `options:`, `name:`; `value: nil`, `id: nil`, `description: nil`, `orientation: :vertical`, `presentation: :list`, `disabled: false`, `required: false`, `size: :md`
|
|
78
|
-
| `Select` | `options: []`, `option_tags: nil`, `id: nil`, `name: nil`, `value: nil`, `include_blank: nil`, `prompt: nil`, `disabled: false`, `required: false`, `multiple: false`, `autocomplete: nil`, `control_html: {}`, `control_aria: {}`, `control_data: {}`
|
|
79
|
-
| `Switch` | `label: nil`, `description: nil`, `id: nil`, `name: nil`, `value: "1"`, `unchecked_value: "0"`, `include_hidden: true`, `checked: false`, `disabled: false`, `required: false`, `invalid: false`, `size: :md`, `control_html: {}`, `control_aria: {}`, `control_data: {}`
|
|
80
|
-
| `Textarea` | `id: nil`, `name: nil`, `value: nil`, `placeholder: nil`, `disabled: false`, `readonly: false`, `required: false`, `autocomplete: nil`, `rows: nil`, `cols: nil`, `minlength: nil`, `maxlength: nil`, `wrap: nil`
|
|
63
|
+
| Component | Constructor-specific options | Root and closed vocabulary | Contract |
|
|
64
|
+
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| `AppearancePicker` | required `id:`; `label:` defaulting to `I18n.t("nitro_kit.appearance_picker.label")`; `label_visible: true`; `presentation: :segmented`; `preference: :system` | `fieldset`, `label`, or `div[data-nk=appearance-picker]`; presentations `segmented radios select dropdown`; preferences `light dark system` | Owns labelled native choices or an icon-only Dropdown trigger with icon-led preference buttons. `preference:` renders the server-persisted choice as the initial `data-state`, checked radio, selected option, and trigger icon, so a stored preference does not flash. It requests changes from and subscribes to the document runtime; zero or many picker instances do not duplicate document media/storage listeners. `id:` follows the shared fragment-safe identifier contract. Segmented selection styling keys off the native `:checked` radio, with `data-state` mirrored only by the controller; enhanced dropdown preference items expose `role="menuitemradio"` with `aria-checked` reflecting the document preference. Preference names come from `nitro_kit.appearance_picker.preferences.*`. `label_visible: false` requires the segmented presentation, renders no legend — browsers give legends special layout — and names the fieldset through `aria-label`; the segmented row fills its container and the segments share the width equally. |
|
|
66
|
+
| `Checkbox` | `label: nil`, `description: nil`, `id: nil`, `name: nil`, `value: "1"`, `unchecked_value: "0"`, `include_hidden: true`, `checked: false`, `indeterminate: false`, `disabled: false`, `required: false`, `invalid: false`, `size: :md`, `control_html: {}`, `control_aria: {}`, `control_data: {}` | `div[data-nk=checkbox]` containing a native checkbox input; sizes `md lg` | Requires label text, block content, or an accessible control name; a description requires a label or block and a non-blank String `id:`, renders outside the label in the `checkbox-description` slot, and binds `aria-describedby`, so the accessible name stays the label text. A named checkbox emits its unchecked hidden input by default, and `include_hidden: true` with a nil `unchecked_value` raises. `invalid: true` sets `aria-invalid` on the control. Checked state stays native; only `indeterminate: true` mounts the `nk--checkable` enhancer, which applies the native DOM property and owns `data-state="indeterminate"`. |
|
|
67
|
+
| `CheckboxGroup` | required `legend:`, `options:`, `name:`; `value: []`, `id: nil`, `description: nil`, `orientation: :vertical`, `presentation: :list`, `unchecked_value: ""`, `include_hidden: true`, `disabled: false`, `required: false`, `size: :md` | native `fieldset[data-nk=checkbox-group]`; orientations `vertical horizontal`; presentations `list cards`; sizes `md lg` | Options are non-empty `Choice` values with unique values and IDs. Names normalize to `[]`; one group-level unchecked input is emitted by default and its `unchecked_value` stays Rails' array sentinel `""`. A name with no id-safe characters raises instead of skipping IDs. `required: true` marks application state as `data-required=true`; it does not emit invalid `aria-required` on the native fieldset or require every checkbox. The legend and description scope the whole group, so choices do not repeat the description through `aria-describedby`. |
|
|
68
|
+
| `ControlGroup` | `label: nil`, `id: nil` | `div[data-nk=control-group]`; optional `role=group` when labelled | Requires direct content. Joins direct Input, Select, and Button children without taking ownership of their values or behavior. `addon(text)` renders a textual prefix, suffix, or unit. The group owns shared borders and logical corner geometry for copy fields, URL builders, and compact filter submissions. |
|
|
69
|
+
| `Dropzone` | required `id:`, `name:`; `label:` defaulting to `I18n.t("nitro_kit.dropzone.label")`, `description: nil`, `presentation: :minimal` (`minimal input avatar compact`), `inline: false`, `direct_upload: true`, `multiple: false`, `accept: nil`, `max_files: 1`, `max_bytes: nil`, `disabled: false`, `required: false` | `div[data-nk=dropzone][data-presentation]`; states `idle drag uploading success error disabled` | Owns a labelled native input, description/error/live status, preview list, native progress, and remove controls. `presentation: :avatar` provides an 80px circular image picker with a live photo preview and upload badge; it requires single-file selection and `inline: false`. `presentation: :compact` provides a small attachment control for table rows. Both retain labelled native inputs, progress, errors, and removal. `inline: true` places the upload icon beside the prompt for a compact horizontal target. File rows sit below the drop target, with image thumbnails or decorative file icons, file size, status, progress, and removal. The live summary remains available to assistive technology without repeating the visible list. `label:` is the visible prompt heading and renders in the `dropzone-title` slot; it replaced the former `title:` keyword. Every other user-facing string comes from the `nitro_kit.dropzone.*` locale scope, and `CONTROLLER_MESSAGE_KEYS` hands the runtime strings to `nk--dropzone` as Stimulus values so no English lives in JavaScript. Limits are positive and consistent; `max_files` must be 1 unless `multiple: true`. Keyboard selection and `direct_upload: false` preserve ordinary form submission. The default `minimal` presentation hides the native input visually while keeping it focusable and named, so the drop target is the only visible affordance; `presentation: :input` keeps the native control visible beside the drop target. |
|
|
70
|
+
| `Field` | optional Rails form builder and field name; see the full signature below | `div[data-nk=field][data-field-type=…]`; types listed below | Default rendering owns label, description, control, and errors. A render block replaces the default composition. `label`, `description`, `control`, and `errors` remain available to custom compositions. The error list keeps native list semantics and uses `aria-live=assertive`; controls reference it through `aria-describedby`. Derived labels use `human_attribute_name` when the form object supplies one. `as: :radio_group` requires a legend and rejects `label: false`, falling back to `I18n.t("nitro_kit.field.options_legend")` when neither an explicit nor a derived label exists. `as: :combobox` binds the field label to `#{id}-input` through `for` and `aria-labelledby` and wires description, error, and invalid state onto the combobox input. |
|
|
71
|
+
| `RichTextArea` | required captured editor content as an `ActiveSupport::SafeBuffer`; `id: nil` | `div[data-nk=rich-text-area]` wrapping `div[data-slot=rich-text-area-editor]` | `Field(as: :rich_text)` through `FormBuilder#field` is the expected path; construct `RichTextArea` directly only for a standalone editor. Wraps trusted output from the host application's rich-text helper. Nitro owns Field composition and theme variables; the editor owns inputs, attachments, and behavior. |
|
|
72
|
+
| `FieldGroup` | no component-specific keywords | `div[data-nk=field-group]` | Requires a content block. It is the default vertical rhythm boundary for a standalone form's visible fields, submit control, and related links. A sibling immediately following the group receives the group gap as `margin-block-start`. |
|
|
73
|
+
| `Fieldset` | `legend: nil`, `description: nil`, `disabled: false`, `name: nil` | native `fieldset[data-nk=fieldset]` | Requires a content block and a legend through the constructor or the matching `legend` compound method; `description` accepts either form too. Declarations may appear anywhere in the block and always render legend → description → fields. |
|
|
74
|
+
| `Input` | `type: :text`, `id: nil`, `name: nil`, `value: nil`, `placeholder: nil`, `disabled: false`, `readonly: false`, `required: false`, `autocomplete: nil`, `min: nil`, `max: nil`, `step: nil`, `minlength: nil`, `maxlength: nil`, `multiple: false`, `accept: nil`, `pattern: nil`, `inputmode: nil`, `checked: nil` | native `input[data-nk=input]`; types `button checkbox color date datetime-local email file hidden month number password radio range search tel text time url week` | Every owned attribute is a keyword; passing one through `html:` raises and names the keyword. `type: :file` with a `value:` raises. Boolean attributes and length constraints are validated; minimum length cannot exceed maximum. Read-only controls have their own muted treatment. `date` receives the Safari editor-alignment fix. `month` and `week` retain their native types only as progressive enhancement: browsers may expose text entry without picker, normalization, or `min`/`max`/`step` enforcement. Applications must server-validate `YYYY-MM` or `YYYY-Www` plus range and step rules, and should compose an application-owned `Select` for exact bounded choices. |
|
|
75
|
+
| `Label` | optional text; `for: nil`, `id: nil` | native `label[data-nk=label]` | Requires non-blank text or a content block. |
|
|
76
|
+
| `RadioButton` | `label: nil`, `description: nil`, `id: nil`, `name: nil`, `value: "1"`, `checked: false`, `disabled: false`, `required: false`, `invalid: false`, `size: :md`, `control_html: {}`, `control_aria: {}`, `control_data: {}` | `div[data-nk=radio-button]` containing a native radio input; sizes `md lg` | Requires label text, block content, or an accessible control name; a description requires a label or block and a non-blank String `id:`, renders outside the label in the `radio-button-description` slot, and binds `aria-describedby`. `invalid: true` sets `aria-invalid` on the control. Selection is entirely native: no Stimulus controller and no mirrored `data-state`. |
|
|
77
|
+
| `RadioButtonGroup` | required `legend:`, `options:`, `name:`; `value: nil`, `id: nil`, `description: nil`, `orientation: :vertical`, `presentation: :list`, `disabled: false`, `required: false`, `size: :md` | native `fieldset[data-nk=radio-button-group]`; orientations `vertical horizontal`; presentations `list cards segmented`; sizes `md lg` | Options are non-empty `Choice` values with unique values and IDs. A non-nil `value:` is compared by string value; `value: nil` selects nothing, including a choice whose value is `""`. A name with no id-safe characters raises instead of skipping IDs. `required: true` marks application state as `data-required=true` and every native radio `required`; it does not emit invalid `aria-required` on the fieldset. The legend and description scope the whole group, so choices do not repeat the description through `aria-describedby`. |
|
|
78
|
+
| `Select` | `options: []`, `option_tags: nil`, `id: nil`, `name: nil`, `value: nil`, `include_blank: nil`, `prompt: nil`, `disabled: false`, `required: false`, `multiple: false`, `autocomplete: nil`, `control_html: {}`, `control_aria: {}`, `control_data: {}` | `span[data-nk=select]` wrapping a native select | `options:` and captured `option_tags:` are mutually exclusive, as are `include_blank:` and `prompt:`. `id:` and the `control_*` bags address the inner select; `html:`, `aria:`, and `data:` address the root span. Multiple names normalize to `[]`, drop the toggle icon, and accept array values. |
|
|
79
|
+
| `Switch` | `label: nil`, `description: nil`, `id: nil`, `name: nil`, `value: "1"`, `unchecked_value: "0"`, `include_hidden: true`, `checked: false`, `disabled: false`, `required: false`, `invalid: false`, `size: :md`, `control_html: {}`, `control_aria: {}`, `control_data: {}` | `div[data-nk=switch]` containing `input[type=checkbox][role=switch]`; sizes `md lg` | Uses native checkbox submission with an unchecked hidden input by default; checked state stays native, with no Stimulus controller and no mirrored `data-state`. Requires label text, block content, or an accessible control name; a description requires a label or block and a non-blank String `id:`, renders outside the label, and is connected with `aria-describedby`. The ARIA-only form renders the bare control and track without a label wrapper. `invalid: true` sets `aria-invalid` on the control. `role` is owned by Switch: a `control_html: { role: ... }` value is replaced by `role="switch"`. |
|
|
80
|
+
| `Textarea` | `id: nil`, `name: nil`, `value: nil`, `placeholder: nil`, `disabled: false`, `readonly: false`, `required: false`, `autocomplete: nil`, `rows: nil`, `cols: nil`, `minlength: nil`, `maxlength: nil`, `wrap: nil` | native `textarea[data-nk=textarea]`; wraps `soft hard off` | Every owned attribute is a keyword; passing one through `html:` raises and names the keyword. Rows and columns must be positive; lengths must be non-negative; minimum length cannot exceed maximum. Read-only controls have their own muted treatment. |
|
|
81
81
|
|
|
82
82
|
`Field` accepts:
|
|
83
83
|
|
|
@@ -134,21 +134,21 @@ Field types are `button color date datetime datetime_local email file hidden mon
|
|
|
134
134
|
|
|
135
135
|
### Structured content and interaction
|
|
136
136
|
|
|
137
|
-
| Component | Constructor-specific options | Root and closed vocabulary | Compound contract
|
|
138
|
-
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
139
|
-
| `Accordion` | required `id:`; `mode: :multiple` | `div[data-nk=accordion]`; modes `multiple single` | Requires one or more uniquely keyed `item(key, title:, expanded: false)` declarations. Each item renders native `details` and `summary`, so find-in-page and fragment navigation reveal matching closed content without a controller. Single mode gives every disclosure the same `name` and accepts at most one initially expanded item; multiple mode omits `name`. There is no disabled item or controller.
|
|
140
|
-
| `Card` | `id: nil` | `article[data-nk=card]`; title levels `1..6` | Requires a content block. `title`, `body`, `footer`, `divider`, and `full` render immediately; their count and order are not constrained. `title`, `body`, and `footer` require non-blank text or a block, and `full` requires a block. The shadowed Phlex elements remain available as `html_title`, `html_body`, and `html_footer`.
|
|
141
|
-
| `Combobox` | required `id:`, `name:`, `label:`, `options:`; `value: nil`, `placeholder: nil`, `include_blank: true`, `placement: :bottom_start`, `required: false`, `disabled: false`, `autocomplete: "off"`, `control_aria: {}` | `div[data-nk=combobox]`; placements `bottom_start bottom_end top_start top_end` | Options are a non-empty set of unique typed choices; `Choice#description` renders as a described secondary line in the listbox. A non-nil value must match a declared option. `label:` renders a real `Label` bound to `#{id}-input`; `label: false` requires `control_aria: { label: }` or `{ labelledby: }` and names the input, listbox, and native select from it. `placeholder:` is the input hint and `include_blank:` is the native blank option. The named native Select is the truthful no-JavaScript control and submission source; Stimulus reveals and synchronizes the searchable combobox enhancement.
|
|
142
|
-
| `Dialog` | required `id:`; `dismissible: true` | `div[data-nk=dialog]` owning exactly one native dialog | Requires a declaration block with exactly one `panel(title:, description: nil, nonmodal: false)` and at most one `trigger`, which forwards Button treatment options including `icon:`, `icon_end:`, and `label:`, matching Sheet and Dropdown. Nitro renders trigger then panel, and inside the panel it owns close button, title, description, then the captured application content, so a sticky close control survives long scrolling content. At most one `close_button(label:)` may be declared inside the panel block, whose label defaults to `I18n.t("nitro_kit.dialog.close")`; `dismissible: false` renders none and declaring one raises. `nonmodal: true` is the explicit server-open state and cannot be combined with a trigger, whose `command="show-modal"` would open the same panel modally. Trigger and close controls use native `command`/`commandfor`. Turbo confirmations are deliberately not a Dialog runtime: applications own inline confirmation dialogs, see [destructive action](patterns/destructive_action.md).
|
|
143
|
-
| `DetailsTable` | `record`, `caption: nil`, `label: nil`, `empty_text: nil`, `boolean_labels: nil`, `route_base: nil`, `id: nil` | `div[data-nk=details-table]` containing a slotted `Table` | Requires one or more fields with unique keys, declared inside the render block. `field(attribute, label: nil, value:)` distinguishes omitted and explicit nil values; a block receives the resolved value. `fields(*attributes)` adds ordinary resolved fields. `caption:` names the table visibly and `label:` names it through `aria-label`; empty and boolean copy remain caller-owned keywords; when omitted they come from `nitro_kit.details_table.empty`, `.boolean_true`, and `.boolean_false`.
|
|
144
|
-
| `Dropdown` | `id: nil`, `placement: :bottom_start` | `div[data-nk=dropdown]`; placements `bottom_start bottom_end top_start top_end`; item variants `default destructive`; item types `button submit reset` | Requires a declaration block, exactly one `trigger`, and at least one `item`. `id:` defaults to a generated `nk-dropdown-*` identifier and otherwise follows the shared fragment-safe identifier contract. `trigger(text = nil, variant:, size:, icon:, icon_end:, label:, disabled:)` forwards to Button, so an icon-only trigger keeps square icon-only geometry; `variant:` and `size:` are validated against Button vocabularies when the trigger is declared. `item(text = nil, href:, icon:, variant:, type:, disabled:)` renders an optional `dropdown-item-icon` slot and emits the owned `data-variant` on the item slot, matching the `Toast::Item` precedent; caller `data-variant` stays reserved. The native `popover=auto` owns open state and implicit invoker relationships. Optional visual titles are presentational to the menu accessibility tree; separators remain native. A small controller adds menu focus navigation, native outside-pointer dismissal fallback for capable Popover engines, and focus restoration without mirroring state. The fallback ignores composed-path interactions inside the trigger or menu and releases its listener on every close and disconnect.
|
|
145
|
-
| `ProgressiveImage` | required `attachment:`; `alt: nil`, `size: :md`, `decorative: false`, `id: nil` | `div[data-nk=progressive-image]`; sizes `sm md lg`; states `empty loading loaded error` | Owns placeholder, image, and fallback slots. The placeholder is decorative and the full image is the only image in the accessibility tree. `alt:` is required unless `decorative: true`, and non-decorative attached images require non-blank alt text. The fallback surfaces that alt text, falling back to `I18n.t("nitro_kit.progressive_image.unavailable")` when there is none. Only an attached image can change after render, so only that fallback is a `role="status"` live region; the never-attached empty state is ordinary static content. `data-enhanced` on the root is the controller-connected flag shared with `AppShell`; it is deliberately not `data-state`, which already carries the closed load-state vocabulary.
|
|
146
|
-
| `Sheet` | required `id:`; `side: :right`, `size: :md`, `close_label:` | layout-transparent `div[data-nk=sheet][data-side][data-size]` owning one native dialog; sides `left right`; sizes `sm md lg` | Requires exactly one Button-backed `trigger` and one `panel(title:, description: nil)`; both carry the shared `html:`, `aria:`, `data:`, and `desperately_need_a_class:` boundary, panel attributes landing on the dialog element as in Dialog. The native modal panel fills the block axis and enters from the selected inline edge. Nitro owns close, title, description, body order, backdrop dismissal, and native focus containment. Use Sheet for contextual narrow navigation or details; use AppShell for whole-application navigation and Dialog for centered decisions.
|
|
147
|
-
| `Table` | `sort: nil`, `direction: nil`, `id: nil`, `table_html: {}`, `table_aria: {}`, `table_data: {}` | `div[data-nk=table]` wrapping a native table; cell alignments `left center right`; header scopes `col row`; directions `asc desc` or nil | Two phases: `caption`, `thead`, and `tbody` collect declarations so the component owns caption → head → body order, while `tr`, `th`, and `td` render immediately inside a collected block and validate before emitting their own markup. Rows must be declared inside a head/body and cells inside a row; text and block content are mutually exclusive. `align: :left` is the default and emits no attribute. Sorting lives on `Table` itself; there is no separate `SortableTable`. `th(text = nil, align: :left, scope: :col, sort: nil, href: nil, sort_data: {})` renders a caller-owned sort link with a unique key, falling back to the humanized sort key when no header text is given; `href:` without `sort:` raises and `sort:` requires a non-blank String `href:`. The active header owns `aria-sort="ascending"`/`"descending"` and a direction Icon, other sortable headers own `aria-sort="none"` and a neutral Icon. `sort:` must be declared inside `thead`. `sort:` and `direction:` are both set or both nil and mirror onto the root as `data-sort` and `data-direction`; `direction:` must be a Symbol or String. A declared caption receives a deterministic id (`<root id>-caption`, generated when the table has no id) and turns the scroll wrapper into a focusable `role="region"` labelled by it; without a caption the wrapper stays a plain div. `table_html:`, `table_aria:`, and `table_data:` address the inner `table` element. |
|
|
148
|
-
| `Tabs` | required `id:`; `default: nil`, `label:` defaulting to `I18n.t("nitro_kit.tabs.label")`, `orientation: :horizontal`, `activation: :automatic` | `div[data-nk=tabs]`; orientations `horizontal vertical`; activations `automatic manual` | Requires one or more uniquely keyed tabs with content and at least one enabled tab. An explicit default must be declared and enabled. Every panel remains available in the no-JavaScript baseline; Stimulus adds APG selection, `hidden="until-found"` inactive panels, find-in-page activation, roving focus, and keyboard activation after enhancement.
|
|
149
|
-
| `Toast` | `duration: 5000`, `label:` defaulting to `I18n.t("nitro_kit.toast.label")`, `id: "nk-toast"` | `section[data-nk=toast][role=region]` wrapping `ol[data-slot=toast-list]`; item variants `default info success warning error` | Accepts zero or more `item(title:, description:, variant:, dismissible:, id:)` declarations inside its render block. Each item requires a title, description, or block. Items carry `role="status"`, or `role="alert"` for the error variant, plus `aria-atomic`, so server-rendered flash items are announced without waiting for a DOM mutation; caller `aria:` colliding with the owned region label or item atomic raises. Every item is `data-turbo-temporary`, so a cached page never replays stale feedback; the region and list survive. The list id is the toast id plus `-list`, so a Turbo Stream can append to it: `turbo_stream.append("nk-toast-list") { render NitroKit::Toast::Item.new(title: "Saved", variant: :success) }`. Items with `dismissible: false` render no dismiss button and are never auto-dismissed; the application owns their removal. `Toast::FlashMessages` maps an enumerable Rails flash into items through explicit keywords. The dismiss control is named from `nitro_kit.toast.dismiss`.
|
|
150
|
-
| `Tooltip` | required `id:`, `content:`; `placement: :top` | `span[data-nk=tooltip]`; placements `top right bottom left` | Requires exactly one trigger. The default `as: NitroKit::Button` path supports native buttons and links; Button options on any other trigger raise. `as: :div` and `:span` create explicit focusable HTML descriptions. `as: :custom` yields `TriggerAttributes(html:, aria:, data:)`; applications must forward all three boundaries to the actual focusable control, including ButtonTo's nested Button or a Dialog trigger. Nitro appends the tooltip ID to existing `aria-describedby`. CSS owns hover/focus visibility; the controller's document-level Escape listener dismisses only while the tooltip is shown, so a wrapping dialog's cancel is not swallowed.
|
|
151
|
-
| `Typeset` | `id: nil` | `div[data-nk=typeset]` | Requires direct content. Styles semantic rich content without constraining its width. Nested Nitro component roots and `data-typeset="off"` regions establish styling boundaries. The shipped stylesheet retains the `@scope` path and includes a low-specificity fallback for engines that do not parse `@scope`, including Firefox through 145. That fallback covers root typography plus explicitly anchored direct-child headings, flow elements, lists, code/pre, tables, and links within those supported elements; it is a documented semantic subset rather than full descendant parity.
|
|
137
|
+
| Component | Constructor-specific options | Root and closed vocabulary | Compound contract |
|
|
138
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
139
|
+
| `Accordion` | required `id:`; `mode: :multiple` | `div[data-nk=accordion]`; modes `multiple single` | Requires one or more uniquely keyed `item(key, title:, expanded: false)` declarations. Each item renders native `details` and `summary`, so find-in-page and fragment navigation reveal matching closed content without a controller. Single mode gives every disclosure the same `name` and accepts at most one initially expanded item; multiple mode omits `name`. There is no disabled item or controller. |
|
|
140
|
+
| `Card` | `id: nil` | `article[data-nk=card]`; title levels `1..6` | Requires a content block. `title`, `body`, `footer`, `divider`, and `full` render immediately; their count and order are not constrained. `title`, `body`, and `footer` require non-blank text or a block, and `full` requires a block. The shadowed Phlex elements remain available as `html_title`, `html_body`, and `html_footer`. |
|
|
141
|
+
| `Combobox` | required `id:`, `name:`, `label:`, `options:`; `value: nil`, `placeholder: nil`, `include_blank: true`, `placement: :bottom_start`, `required: false`, `disabled: false`, `autocomplete: "off"`, `control_aria: {}` | `div[data-nk=combobox]`; placements `bottom_start bottom_end top_start top_end` | Options are a non-empty set of unique typed choices; `Choice#description` renders as a described secondary line in the listbox. A non-nil value must match a declared option. `label:` renders a real `Label` bound to `#{id}-input`; `label: false` requires `control_aria: { label: }` or `{ labelledby: }` and names the input, listbox, and native select from it. `placeholder:` is the input hint and `include_blank:` is the native blank option. The named native Select is the truthful no-JavaScript control and submission source; Stimulus reveals and synchronizes the searchable combobox enhancement. |
|
|
142
|
+
| `Dialog` | required `id:`; `dismissible: true` | `div[data-nk=dialog]` owning exactly one native dialog | Requires a declaration block with exactly one `panel(title:, description: nil, nonmodal: false)` and at most one `trigger`, which forwards Button treatment options including `icon:`, `icon_end:`, and `label:`, matching Sheet and Dropdown. Nitro renders trigger then panel, and inside the panel it owns close button, title, description, then the captured application content, so a sticky close control survives long scrolling content. At most one `close_button(label:)` may be declared inside the panel block, whose label defaults to `I18n.t("nitro_kit.dialog.close")`; `dismissible: false` renders none and declaring one raises. `nonmodal: true` is the explicit server-open state and cannot be combined with a trigger, whose `command="show-modal"` would open the same panel modally. Trigger and close controls use native `command`/`commandfor`. Turbo confirmations are deliberately not a Dialog runtime: applications own inline confirmation dialogs, see [destructive action](patterns/destructive_action.md). |
|
|
143
|
+
| `DetailsTable` | `record`, `caption: nil`, `label: nil`, `empty_text: nil`, `boolean_labels: nil`, `route_base: nil`, `id: nil` | `div[data-nk=details-table]` containing a slotted `Table` | Requires one or more fields with unique keys, declared inside the render block. `field(attribute, label: nil, value:)` distinguishes omitted and explicit nil values; a block receives the resolved value. `fields(*attributes)` adds ordinary resolved fields. `caption:` names the table visibly and `label:` names it through `aria-label`; empty and boolean copy remain caller-owned keywords; when omitted they come from `nitro_kit.details_table.empty`, `.boolean_true`, and `.boolean_false`. |
|
|
144
|
+
| `Dropdown` | `id: nil`, `placement: :bottom_start` | `div[data-nk=dropdown]`; placements `bottom_start bottom_end top_start top_end`; item variants `default destructive`; item types `button submit reset` | Requires a declaration block, exactly one `trigger`, and at least one `item`. `id:` defaults to a generated `nk-dropdown-*` identifier and otherwise follows the shared fragment-safe identifier contract. `trigger(text = nil, variant:, size:, icon:, icon_end:, label:, disabled:)` forwards to Button, so an icon-only trigger keeps square icon-only geometry; `variant:` and `size:` are validated against Button vocabularies when the trigger is declared. `item(text = nil, href:, icon:, variant:, type:, disabled:)` renders an optional `dropdown-item-icon` slot and emits the owned `data-variant` on the item slot, matching the `Toast::Item` precedent; caller `data-variant` stays reserved. The native `popover=auto` owns open state and implicit invoker relationships. Optional visual titles are presentational to the menu accessibility tree; separators remain native. A small controller adds menu focus navigation, native outside-pointer dismissal fallback for capable Popover engines, and focus restoration without mirroring state. The fallback ignores composed-path interactions inside the trigger or menu and releases its listener on every close and disconnect. |
|
|
145
|
+
| `ProgressiveImage` | required `attachment:`; `alt: nil`, `size: :md`, `decorative: false`, `id: nil` | `div[data-nk=progressive-image]`; sizes `sm md lg`; states `empty loading loaded error` | Owns placeholder, image, and fallback slots. The placeholder is decorative and the full image is the only image in the accessibility tree. `alt:` is required unless `decorative: true`, and non-decorative attached images require non-blank alt text. The fallback surfaces that alt text, falling back to `I18n.t("nitro_kit.progressive_image.unavailable")` when there is none. Only an attached image can change after render, so only that fallback is a `role="status"` live region; the never-attached empty state is ordinary static content. `data-enhanced` on the root is the controller-connected flag shared with `AppShell`; it is deliberately not `data-state`, which already carries the closed load-state vocabulary. |
|
|
146
|
+
| `Sheet` | required `id:`; `side: :right`, `size: :md`, `close_label:` | layout-transparent `div[data-nk=sheet][data-side][data-size]` owning one native dialog; sides `left right`; sizes `sm md lg` | Requires exactly one Button-backed `trigger` and one `panel(title:, description: nil)`; both carry the shared `html:`, `aria:`, `data:`, and `desperately_need_a_class:` boundary, panel attributes landing on the dialog element as in Dialog. The native modal panel fills the block axis and enters from the selected inline edge. Nitro owns close, title, description, body order, backdrop dismissal, and native focus containment. Use Sheet for contextual narrow navigation or details; use AppShell for whole-application navigation and Dialog for centered decisions. |
|
|
147
|
+
| `Table` | `sort: nil`, `direction: nil`, `id: nil`, `table_html: {}`, `table_aria: {}`, `table_data: {}` | `div[data-nk=table]` wrapping a native table; cell alignments `left center right`; header scopes `col row`; directions `asc desc` or nil | Every table uses its horizontal scroll wrapper and non-wrapping cells at all viewport widths; columns and controls retain their natural size. Two phases: `caption`, `thead`, and `tbody` collect declarations so the component owns caption → head → body order, while `tr`, `th`, and `td` render immediately inside a collected block and validate before emitting their own markup. Rows must be declared inside a head/body and cells inside a row; text and block content are mutually exclusive. `align: :left` is the default and emits no attribute. Sorting lives on `Table` itself; there is no separate `SortableTable`. `th(text = nil, align: :left, scope: :col, sort: nil, href: nil, sort_data: {})` renders a caller-owned sort link with a unique key, falling back to the humanized sort key when no header text is given; `href:` without `sort:` raises and `sort:` requires a non-blank String `href:`. The active header owns `aria-sort="ascending"`/`"descending"` and a direction Icon, other sortable headers own `aria-sort="none"` and a neutral Icon. `sort:` must be declared inside `thead`. `sort:` and `direction:` are both set or both nil and mirror onto the root as `data-sort` and `data-direction`; `direction:` must be a Symbol or String. A declared caption receives a deterministic id (`<root id>-caption`, generated when the table has no id) and turns the scroll wrapper into a focusable `role="region"` labelled by it; without a caption the wrapper stays a plain div. `table_html:`, `table_aria:`, and `table_data:` address the inner `table` element. |
|
|
148
|
+
| `Tabs` | required `id:`; `default: nil`, `label:` defaulting to `I18n.t("nitro_kit.tabs.label")`, `orientation: :horizontal`, `activation: :automatic` | `div[data-nk=tabs]`; orientations `horizontal vertical`; activations `automatic manual` | Requires one or more uniquely keyed tabs with content and at least one enabled tab. An explicit default must be declared and enabled. Every panel remains available in the no-JavaScript baseline; Stimulus adds APG selection, `hidden="until-found"` inactive panels, find-in-page activation, roving focus, and keyboard activation after enhancement. |
|
|
149
|
+
| `Toast` | `duration: 5000`, `label:` defaulting to `I18n.t("nitro_kit.toast.label")`, `id: "nk-toast"` | `section[data-nk=toast][role=region]` wrapping `ol[data-slot=toast-list]`; item variants `default info success warning error` | Accepts zero or more `item(title:, description:, variant:, dismissible:, id:)` declarations inside its render block. Each item requires a title, description, or block. Items carry `role="status"`, or `role="alert"` for the error variant, plus `aria-atomic`, so server-rendered flash items are announced without waiting for a DOM mutation; caller `aria:` colliding with the owned region label or item atomic raises. Every item is `data-turbo-temporary`, so a cached page never replays stale feedback; the region and list survive. The list id is the toast id plus `-list`, so a Turbo Stream can append to it: `turbo_stream.append("nk-toast-list") { render NitroKit::Toast::Item.new(title: "Saved", variant: :success) }`. Items with `dismissible: false` render no dismiss button and are never auto-dismissed; the application owns their removal. `Toast::FlashMessages` maps an enumerable Rails flash into items through explicit keywords. The dismiss control is named from `nitro_kit.toast.dismiss`. |
|
|
150
|
+
| `Tooltip` | required `id:`, `content:`; `placement: :top` | `span[data-nk=tooltip]`; placements `top right bottom left` | Requires exactly one trigger. The default `as: NitroKit::Button` path supports native buttons and links; Button options on any other trigger raise. `as: :div` and `:span` create explicit focusable HTML descriptions. `as: :custom` yields `TriggerAttributes(html:, aria:, data:)`; applications must forward all three boundaries to the actual focusable control, including ButtonTo's nested Button or a Dialog trigger. Nitro appends the tooltip ID to existing `aria-describedby`. CSS owns hover/focus visibility; the controller's document-level Escape listener dismisses only while the tooltip is shown, so a wrapping dialog's cancel is not swallowed. |
|
|
151
|
+
| `Typeset` | `id: nil` | `div[data-nk=typeset]` | Requires direct content. Styles semantic rich content without constraining its width. Nested Nitro component roots and `data-typeset="off"` regions establish styling boundaries. The shipped stylesheet retains the `@scope` path and includes a low-specificity fallback for engines that do not parse `@scope`, including Firefox through 145. That fallback covers root typography plus explicitly anchored direct-child headings, flow elements, lists, code/pre, tables, and links within those supported elements; it is a documented semantic subset rather than full descendant parity. |
|
|
152
152
|
|
|
153
153
|
## Layout primitives
|
|
154
154
|
|
|
@@ -207,19 +207,19 @@ Unknown values or prefixes, duplicate base/prefix tokens, missing base values, b
|
|
|
207
207
|
|
|
208
208
|
## Application layout and page sections
|
|
209
209
|
|
|
210
|
-
| Component | Constructor | Root
|
|
211
|
-
| ----------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
212
|
-
| `AuthShell` | `id: nil` | `main[data-nk=auth-shell]`
|
|
213
|
-
| `AppShell` | required `id:`; `layout: :sidebar`; configurable skip/open/close/dialog labels defaulting to `nitro_kit.app_shell.*` | `div[data-nk=app-shell][data-layout]`; layouts `sidebar topbar
|
|
214
|
-
| `SettingsLayout` | `id: nil` | `div[data-nk=settings-layout]`
|
|
215
|
-
| `Toolbar` | `id: nil` | `div[data-nk=toolbar]`
|
|
216
|
-
| `PaginationBar` | `id: nil` | `div[data-nk=pagination-bar]`
|
|
217
|
-
| `PageHeader` | `title: nil`, `eyebrow: nil`, `description: nil`, `level: 1`, `id: nil` | `header[data-nk=page-header]`; title levels `1..6`
|
|
218
|
-
| `StatGrid` | `cols: "1 sm:2 lg:3"`, `gap: 4`, `id: nil` | `div[data-nk=stat-grid]` wrapping a slotted `Grid` and one `dl`
|
|
219
|
-
| `DataSection` | `title: nil`, `description: nil`, `level: 2`, `id: nil` | `section[data-nk=data-section][aria-labelledby]` naming the title heading; title levels `1..6`
|
|
220
|
-
| `SettingsSection` | `title: nil`, `description: nil`, `level: 2`, `id: nil` | `section[data-nk=settings-section][aria-labelledby]` naming the title heading; title levels `1..6`
|
|
221
|
-
| `DangerZone` | `title: nil`, `description: nil`, `level: 2`, `id: nil` | `section[data-nk=danger-zone]`; title levels `1..6`
|
|
222
|
-
| `EmptyState` | `title: nil`, `description: nil`, `variant: :default`, `level: 2`, `id: nil` | `section[data-nk=empty-state][data-variant]`; variants `default borderless`
|
|
210
|
+
| Component | Constructor | Root | Compound contract |
|
|
211
|
+
| ----------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
212
|
+
| `AuthShell` | `id: nil` | `main[data-nk=auth-shell]` | Requires direct content. Owns `main` → medium Container → `Flex(dir: :col, gap: 6, align: :stretch)`; branding, Cards, and Turbo boundaries remain caller-owned. |
|
|
213
|
+
| `AppShell` | required `id:`; `layout: :sidebar`; configurable skip/open/close/dialog labels defaulting to `nitro_kit.app_shell.*` | `div[data-nk=app-shell][data-layout]`; layouts `sidebar topbar`; states `open closed`; `data-enhanced` is written by the Stimulus controller and reserved | Requires exactly one `navigation` and one `main`; optional `brand` and `topbar` are unique. Regions are declared inside the render block only. One navigation tree moves between its neutral desktop wrapper and a native modal dialog at narrow widths. IDs are fragment-safe; product policy remains caller-owned. |
|
|
214
|
+
| `SettingsLayout` | `id: nil` | `div[data-nk=settings-layout]` | Exactly one `navigation(label:)` and one `content` region, both block-only and declared inside the render block. The navigation requires at least one `item(text, href:, icon: nil, current: false, html:, aria:, data:, desperately_need_a_class:)` and renders `nav > ul > li > a`, where `icon:` names a Lucide icon and the item attribute bags land on the link; a current item uses `aria-current="page"` and at most one item is current. Routes remain caller-owned. |
|
|
215
|
+
| `Toolbar` | `id: nil` | `div[data-nk=toolbar]` | At most one `leading` and one `trailing`; at least one region total. Each region requires a content block. It deliberately has no toolbar role, sticky mode, or action registry. |
|
|
216
|
+
| `PaginationBar` | `id: nil` | `div[data-nk=pagination-bar]` | Exactly one typed `pagination(NitroKit::Pagination)` and at most one non-blank `summary`. The summary announces politely unless the caller supplies its own `aria-live`; the shadowed Phlex element remains available as `html_summary`. Counts, routes, and page math remain caller-owned. |
|
|
217
|
+
| `PageHeader` | `title: nil`, `eyebrow: nil`, `description: nil`, `level: 1`, `id: nil` | `header[data-nk=page-header]`; title levels `1..6` | Requires `title` through constructor text or its matching compound method. `eyebrow`, `title`, and `description` each accept text or a rich-content block exactly once and retain fixed eyebrow → heading → description order; at most one `ButtonGroup` through `actions`. |
|
|
218
|
+
| `StatGrid` | `cols: "1 sm:2 lg:3"`, `gap: 4`, `id: nil` | `div[data-nk=stat-grid]` wrapping a slotted `Grid` and one `dl` | One or more `stat(key:, label:, value:, detail:)` declarations with unique normalized keys. Values accept any object and render `to_s`; blank copy raises. `cols:` and `gap:` use the `Grid` responsive vocabulary. Stats are declared inside the render block; the root spans the full inline size. |
|
|
219
|
+
| `DataSection` | `title: nil`, `description: nil`, `level: 2`, `id: nil` | `section[data-nk=data-section][aria-labelledby]` naming the title heading; title levels `1..6` | Requires `title`, declared inside the render block; `title` and `description` each accept constructor text or one matching text/rich-content compound declaration. Exactly one `table(Table or DetailsTable)` or level-3 `empty_state(EmptyState)`; at most one `Button` or `ButtonGroup` through `actions`. The shadowed Phlex element remains available as `html_table`. |
|
|
220
|
+
| `SettingsSection` | `title: nil`, `description: nil`, `level: 2`, `id: nil` | `section[data-nk=settings-section][aria-labelledby]` naming the title heading; title levels `1..6` | Requires `title`; `title` and `description` each accept constructor text or one matching text/rich-content compound declaration, and `title` also accepts `level:`. Exactly one `form` block; at most one `Alert` through `status`. |
|
|
221
|
+
| `DangerZone` | `title: nil`, `description: nil`, `level: 2`, `id: nil` | `section[data-nk=danger-zone]`; title levels `1..6` | Requires `title` and `description`, each through constructor text or one matching text/rich-content compound declaration. Exactly one `confirmation` block and at most one non-destructive Button through `escape`; the description renders in the `danger-zone-description` slot. |
|
|
222
|
+
| `EmptyState` | `title: nil`, `description: nil`, `variant: :default`, `level: 2`, `id: nil` | `section[data-nk=empty-state][data-variant]`; variants `default borderless` | Requires `title`; `title` and `description` each accept constructor text or one matching text/rich-content compound declaration. Heading levels `2..6`; at most one `Icon` and two distinct Button actions. `default` draws the dashed frame for a standalone region; `borderless` drops frame and fill for a region a Card, DataSection, or table already encloses, where the dashed frame would read as a Dropzone. Declaring a compound method outside the render block raises. |
|
|
223
223
|
|
|
224
224
|
## Non-visual Rails integration
|
|
225
225
|
|
|
@@ -287,3 +287,9 @@ interaction merely from server-rendered markup in this table.
|
|
|
287
287
|
The interactive theme customizer is documentation-site software rather than a component contract. The gallery instead verifies the theming contract itself: the documented token set, the set declared in `src/stylesheets/nitro_kit/tokens.css`, and the set the bundled stylesheet serves are the same set, component CSS consumes only declared public tokens, and scoped `--nk-*` overrides on an application-owned wrapper reach Nitro descendants through inheritance.
|
|
288
288
|
|
|
289
289
|
The public [customization guide](customization.md) covers the complete token catalog, load order, scoped overrides, light/dark/system selectors, appearance and CSP setup, customizer-export installation, shell composition, and copyable Rails examples.
|
|
290
|
+
|
|
291
|
+
Dialog panels reset text alignment to `start`. The `dialog-header` keeps the
|
|
292
|
+
title and description clear of the sticky corner close control; `dialog-body`
|
|
293
|
+
owns the application content below it. `close_button` configures that corner
|
|
294
|
+
control, not a footer action. Compose visible Cancel and submit Buttons in a
|
|
295
|
+
right-aligned Flex row as shown in the destructive-action pattern.
|