nitro_kit 0.9.0 → 2.0.0.alpha.2
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/.agents/plugins/marketplace.json +20 -0
- data/CHANGELOG.md +172 -0
- data/LICENSE +28 -0
- data/README.md +22 -15
- data/STYLE_GUIDE.md +425 -0
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
- data/app/assets/stylesheets/nitro_kit.css +7239 -0
- data/app/components/nitro_kit/accordion.rb +125 -65
- data/app/components/nitro_kit/alert.rb +80 -43
- data/app/components/nitro_kit/app_navigation.rb +307 -0
- data/app/components/nitro_kit/app_shell.rb +227 -0
- data/app/components/nitro_kit/appearance_bootstrap.rb +146 -0
- data/app/components/nitro_kit/appearance_picker.rb +156 -0
- data/app/components/nitro_kit/auth_shell.rb +34 -0
- data/app/components/nitro_kit/avatar.rb +85 -29
- data/app/components/nitro_kit/avatar_stack.rb +155 -9
- data/app/components/nitro_kit/badge.rb +43 -81
- data/app/components/nitro_kit/button.rb +180 -103
- data/app/components/nitro_kit/button_group.rb +63 -11
- data/app/components/nitro_kit/button_to.rb +69 -0
- data/app/components/nitro_kit/card.rb +42 -45
- data/app/components/nitro_kit/checkbox.rb +115 -62
- data/app/components/nitro_kit/checkbox_group.rb +131 -22
- data/app/components/nitro_kit/choice.rb +55 -0
- data/app/components/nitro_kit/combobox.rb +292 -105
- data/app/components/nitro_kit/command_palette.rb +412 -0
- data/app/components/nitro_kit/component.rb +322 -46
- data/app/components/nitro_kit/container.rb +32 -0
- data/app/components/nitro_kit/control_group.rb +45 -0
- data/app/components/nitro_kit/danger_zone.rb +78 -0
- data/app/components/nitro_kit/data_section.rb +106 -0
- data/app/components/nitro_kit/details_table.rb +229 -0
- data/app/components/nitro_kit/dialog.rb +257 -83
- data/app/components/nitro_kit/dropdown.rb +335 -122
- data/app/components/nitro_kit/dropzone.rb +326 -0
- data/app/components/nitro_kit/empty_state.rb +99 -0
- data/app/components/nitro_kit/field.rb +461 -236
- data/app/components/nitro_kit/field_group.rb +15 -8
- data/app/components/nitro_kit/fieldset.rb +37 -39
- data/app/components/nitro_kit/flex.rb +49 -0
- data/app/components/nitro_kit/form_builder.rb +313 -72
- data/app/components/nitro_kit/form_section.rb +79 -0
- data/app/components/nitro_kit/grid.rb +34 -0
- data/app/components/nitro_kit/icon.rb +54 -25
- data/app/components/nitro_kit/input.rb +97 -10
- data/app/components/nitro_kit/label.rb +22 -6
- data/app/components/nitro_kit/layout_options.rb +7 -0
- data/app/components/nitro_kit/page_header.rb +76 -0
- data/app/components/nitro_kit/pagination.rb +355 -69
- data/app/components/nitro_kit/pagination_bar.rb +90 -0
- data/app/components/nitro_kit/progressive_image.rb +181 -0
- data/app/components/nitro_kit/radio_button.rb +77 -55
- data/app/components/nitro_kit/radio_button_group.rb +107 -36
- data/app/components/nitro_kit/responsive_value.rb +94 -0
- data/app/components/nitro_kit/rich_text_area.rb +31 -0
- data/app/components/nitro_kit/select.rb +129 -64
- data/app/components/nitro_kit/settings_layout.rb +152 -0
- data/app/components/nitro_kit/sheet.rb +202 -0
- data/app/components/nitro_kit/stat_grid.rb +75 -0
- data/app/components/nitro_kit/switch.rb +108 -51
- data/app/components/nitro_kit/table.rb +237 -45
- data/app/components/nitro_kit/tabs.rb +168 -63
- data/app/components/nitro_kit/textarea.rb +81 -10
- data/app/components/nitro_kit/toast.rb +211 -80
- data/app/components/nitro_kit/toolbar.rb +75 -0
- data/app/components/nitro_kit/tooltip.rb +235 -34
- data/app/components/nitro_kit/typeset.rb +28 -0
- data/app/javascript/controllers/nk/app_shell_controller.js +224 -0
- data/app/javascript/controllers/nk/appearance_controller.js +73 -0
- data/app/javascript/controllers/nk/avatar_controller.js +15 -0
- data/app/javascript/controllers/nk/button_controller.js +56 -0
- data/app/javascript/controllers/nk/checkable_controller.js +41 -0
- data/app/javascript/controllers/nk/combobox_controller.js +314 -82
- data/app/javascript/controllers/nk/command_palette_controller.js +249 -0
- data/app/javascript/controllers/nk/dialog_controller.js +14 -10
- data/app/javascript/controllers/nk/dropdown_controller.js +106 -58
- data/app/javascript/controllers/nk/dropzone/direct_upload.js +61 -0
- data/app/javascript/controllers/nk/dropzone/file_rules.js +77 -0
- data/app/javascript/controllers/nk/dropzone/form_submit_lock.js +30 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +482 -0
- data/app/javascript/controllers/nk/overlay_position.js +37 -0
- data/app/javascript/controllers/nk/progressive_image_controller.js +116 -0
- data/app/javascript/controllers/nk/tabs_controller.js +135 -23
- data/app/javascript/controllers/nk/toast_controller.js +112 -45
- data/app/javascript/controllers/nk/tooltip_controller.js +12 -47
- data/config/importmap.rb +2 -0
- data/config/locales/en.yml +98 -0
- data/docs/agent_guide.md +139 -0
- data/docs/agent_native_spec.md +422 -0
- data/docs/component_contracts.md +263 -0
- data/docs/customization.md +595 -0
- data/docs/hotwire.md +94 -0
- data/docs/initialization_prompt.md +36 -0
- data/docs/migration_1_to_2.md +318 -0
- data/docs/new_app_strategy.md +22 -0
- data/docs/patterns/application_foundation.md +136 -0
- data/docs/patterns/crud_resource.md +144 -0
- data/docs/patterns/destructive_action.md +105 -0
- data/docs/patterns/flash_and_toast.md +57 -0
- data/docs/patterns/inline_edit.md +59 -0
- data/docs/patterns/queryable_collection.md +187 -0
- data/docs/patterns/resource_form.md +126 -0
- data/docs/rails_conventions.md +95 -0
- data/docs/rails_integration.md +514 -0
- data/lib/generators/nitro_kit/install_generator.rb +48 -0
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +67 -0
- data/lib/nitro_kit/engine.rb +20 -0
- data/lib/nitro_kit/installation.rb +671 -0
- data/lib/nitro_kit/migration_inventory.rb +345 -0
- data/lib/nitro_kit/upgrade_smoke_test.rb +403 -0
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/nitro_kit.rb +5 -42
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +42 -0
- data/lib/tasks/nitro_kit_tasks.rake +79 -4
- data/plugins/nitro-kit/.codex-plugin/plugin.json +28 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +67 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +60 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +62 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +4 -0
- data/src/stylesheets/nitro_kit/components/accordion.css +119 -0
- data/src/stylesheets/nitro_kit/components/alert.css +82 -0
- data/src/stylesheets/nitro_kit/components/app_navigation.css +156 -0
- data/src/stylesheets/nitro_kit/components/app_shell.css +605 -0
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +117 -0
- data/src/stylesheets/nitro_kit/components/auth_shell.css +8 -0
- data/src/stylesheets/nitro_kit/components/avatar.css +81 -0
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +80 -0
- data/src/stylesheets/nitro_kit/components/badge.css +76 -0
- data/src/stylesheets/nitro_kit/components/button.css +283 -0
- data/src/stylesheets/nitro_kit/components/button_group.css +53 -0
- data/src/stylesheets/nitro_kit/components/button_to.css +6 -0
- data/src/stylesheets/nitro_kit/components/card.css +70 -0
- data/src/stylesheets/nitro_kit/components/checkbox.css +160 -0
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +61 -0
- data/src/stylesheets/nitro_kit/components/combobox.css +148 -0
- data/src/stylesheets/nitro_kit/components/command_palette.css +255 -0
- data/src/stylesheets/nitro_kit/components/container.css +25 -0
- data/src/stylesheets/nitro_kit/components/control_group.css +149 -0
- data/src/stylesheets/nitro_kit/components/danger_zone.css +54 -0
- data/src/stylesheets/nitro_kit/components/data_section.css +40 -0
- data/src/stylesheets/nitro_kit/components/details_table.css +53 -0
- data/src/stylesheets/nitro_kit/components/dialog.css +80 -0
- data/src/stylesheets/nitro_kit/components/dropdown.css +152 -0
- data/src/stylesheets/nitro_kit/components/dropzone.css +198 -0
- data/src/stylesheets/nitro_kit/components/empty_state.css +53 -0
- data/src/stylesheets/nitro_kit/components/field.css +79 -0
- data/src/stylesheets/nitro_kit/components/field_group.css +6 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +28 -0
- data/src/stylesheets/nitro_kit/components/flex.css +455 -0
- data/src/stylesheets/nitro_kit/components/form_section.css +71 -0
- data/src/stylesheets/nitro_kit/components/grid.css +308 -0
- data/src/stylesheets/nitro_kit/components/icon.css +26 -0
- data/src/stylesheets/nitro_kit/components/input.css +80 -0
- data/src/stylesheets/nitro_kit/components/label.css +9 -0
- data/src/stylesheets/nitro_kit/components/layout.css +440 -0
- data/src/stylesheets/nitro_kit/components/page_header.css +61 -0
- data/src/stylesheets/nitro_kit/components/pagination.css +119 -0
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +41 -0
- data/src/stylesheets/nitro_kit/components/palette.css +113 -0
- data/src/stylesheets/nitro_kit/components/progressive_image.css +139 -0
- data/src/stylesheets/nitro_kit/components/radio_button.css +127 -0
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +143 -0
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +21 -0
- data/src/stylesheets/nitro_kit/components/select.css +70 -0
- data/src/stylesheets/nitro_kit/components/settings_layout.css +100 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +100 -0
- data/src/stylesheets/nitro_kit/components/stat_grid.css +51 -0
- data/src/stylesheets/nitro_kit/components/switch.css +152 -0
- data/src/stylesheets/nitro_kit/components/table.css +116 -0
- data/src/stylesheets/nitro_kit/components/tabs.css +138 -0
- data/src/stylesheets/nitro_kit/components/textarea.css +39 -0
- data/src/stylesheets/nitro_kit/components/toast.css +131 -0
- data/src/stylesheets/nitro_kit/components/toolbar.css +78 -0
- data/src/stylesheets/nitro_kit/components/tooltip.css +124 -0
- data/src/stylesheets/nitro_kit/components/typeset.css +191 -0
- data/src/stylesheets/nitro_kit/layers.css +7 -0
- data/src/stylesheets/nitro_kit/reset.css +192 -0
- data/src/stylesheets/nitro_kit/tokens.css +198 -0
- metadata +182 -51
- data/Rakefile +0 -8
- data/app/assets/tailwind/application.css +0 -49
- data/app/components/nitro_kit/datepicker.rb +0 -9
- data/app/helpers/nitro_kit/accordion_helper.rb +0 -9
- data/app/helpers/nitro_kit/alert_helper.rb +0 -11
- data/app/helpers/nitro_kit/avatar_helper.rb +0 -13
- data/app/helpers/nitro_kit/badge_helper.rb +0 -13
- data/app/helpers/nitro_kit/button_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/button_helper.rb +0 -49
- data/app/helpers/nitro_kit/card_helper.rb +0 -9
- data/app/helpers/nitro_kit/checkbox_helper.rb +0 -50
- data/app/helpers/nitro_kit/combobox_helper.rb +0 -9
- data/app/helpers/nitro_kit/datepicker_helper.rb +0 -9
- data/app/helpers/nitro_kit/dialog_helper.rb +0 -9
- data/app/helpers/nitro_kit/dropdown_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_helper.rb +0 -9
- data/app/helpers/nitro_kit/fieldset_helper.rb +0 -9
- data/app/helpers/nitro_kit/form_helper.rb +0 -13
- data/app/helpers/nitro_kit/icon_helper.rb +0 -9
- data/app/helpers/nitro_kit/input_helper.rb +0 -37
- data/app/helpers/nitro_kit/label_helper.rb +0 -19
- data/app/helpers/nitro_kit/pagination_helper.rb +0 -46
- data/app/helpers/nitro_kit/radio_button_helper.rb +0 -23
- data/app/helpers/nitro_kit/select_helper.rb +0 -24
- data/app/helpers/nitro_kit/switch_helper.rb +0 -9
- data/app/helpers/nitro_kit/table_helper.rb +0 -9
- data/app/helpers/nitro_kit/tabs_helper.rb +0 -9
- data/app/helpers/nitro_kit/textarea_helper.rb +0 -9
- data/app/helpers/nitro_kit/toast_helper.rb +0 -25
- data/app/helpers/nitro_kit/tooltip_helper.rb +0 -9
- data/app/javascript/controllers/nk/accordion_controller.js +0 -18
- data/app/javascript/controllers/nk/datepicker_controller.js +0 -5
- data/app/javascript/controllers/nk/switch_controller.js +0 -32
- data/lib/generators/nitro_kit/component_generator.rb +0 -84
- data/lib/nitro_kit/schema_builder.rb +0 -129
- data/lib/nitro_kit/variants.rb +0 -21
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Nitro Kit 2.0 component contracts
|
|
2
|
+
|
|
3
|
+
This is the shipped public catalog for `2.0.0.alpha.2`. It describes current Ruby construction, rendered roots, closed vocabularies, compound cardinalities, and integration boundaries. Other candidate abstractions and the historical delivery plan are not public API.
|
|
4
|
+
|
|
5
|
+
## Shared component boundary
|
|
6
|
+
|
|
7
|
+
Every visual component accepts:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
html: {}, aria: {}, data: {}, desperately_need_a_class: nil
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Many components also expose `id:` or other common native semantics directly. Less common native attributes belong in `html:`. ARIA attributes belong in `aria:` and application data attributes belong in `data:`.
|
|
14
|
+
|
|
15
|
+
Nitro Kit reserves `data-nk`, `data-slot`, `data-variant`, `data-size`, `data-nk-escape`, `data-enhanced`, and the component-owned `data-state`, `data-disabled`, `data-required`, `data-orientation`, `data-presentation`, `data-placement`, `data-layout`, `data-side`, and `data-field-type`. Passing any of them through `data:` raises "reserved by Nitro Kit". Application `data-controller` and `data-action` values compose additively with Nitro behavior; other owned-data collisions raise. `class` and `style` are rejected, including inside `html:`.
|
|
16
|
+
|
|
17
|
+
The single styling escape is `desperately_need_a_class:`. It requires a non-blank string and emits both the class and `data-nk-escape="class"`. There is no untyped structural bypass.
|
|
18
|
+
|
|
19
|
+
Every default string a person can read or hear comes from the engine-loaded `nitro_kit.*` locale scope, so a keyword documented below as "defaulting to `I18n.t(...)`" resolves through the host application's locale. Passing the keyword explicitly always wins.
|
|
20
|
+
|
|
21
|
+
`variant:` and `size:` on a root are the component's identity axes and are emitted only by the base component. A slot carries its own `data-variant` only when it has variant identity of its own — `Toast::Item` through the nested-component root channel, Dropdown items through `slot_attributes(:item, variant:)`. Caller `data: { variant: }` is reserved in both cases.
|
|
22
|
+
|
|
23
|
+
## Atoms and components
|
|
24
|
+
|
|
25
|
+
### Actions, display, and navigation
|
|
26
|
+
|
|
27
|
+
| Component | Constructor-specific options | Root and closed vocabulary | Compound contract |
|
|
28
|
+
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
29
|
+
| `Alert` | `variant: :default`, `title: nil`, `description: nil`, `live: :off`, `id: nil` | `div[data-nk=alert]`; variants `default info success warning error`, the same vocabulary as `Toast::Item` with `default` as the neutral one; live modes `off polite assertive` | One semantic axis: `variant:` emits `data-variant` and drives the tint through `Alert::VARIANT_PALETTE`, Alert-owned and independent of the Badge color axis. There is no `color:` option. Accepts at most one `NitroKit::Icon` through `icon`, plus `title` and `description` through constructor text or the matching compound method, never both. Nitro renders icon, title, then description regardless of declaration order; declaring outside the render block raises. `live: :polite` adds `role=status`; `live: :assertive` adds `role=alert`; static alerts have no live role by default. |
|
|
30
|
+
| `AppNavigation` | required `label:`; `id: nil` | native `nav[data-nk=app-navigation]` | Requires one body and at least one item. The body is a `ul`; every entry is an `li`. Optional unique `header` and `footer` surround ordered `section(label: nil)`, `divider`, `item(text, href:, icon: nil, badge: nil, badge_color: :neutral, current: false, html:, aria:, data:, desperately_need_a_class:)`, and at most one `spacer`. A section renders a `span` label plus a nested `ul` named through `aria-label`; it emits no heading. Items render `li > a[data-slot=app-navigation-item-link]` and carry the item attribute bags. Icon and badge vocabularies are validated as the item is declared. At most one item is current. |
|
|
31
|
+
| `Avatar` | `src: nil`, `alt: ""`, `fallback: nil`, `size: :md`, `loading: "lazy"`, `decoding: "async"`, `id: nil` | `span[data-nk=avatar]`; sizes `xs sm md lg`; state `error` | Renders fallback and optional image slots. The source is a keyword; there is no positional form. When a source is present the root carries `nk--avatar`, which sets `data-state="error"` if the image fails to load so the initials fallback shows through instead of a broken-image glyph, and unhides that fallback for assistive technology. |
|
|
32
|
+
| `AvatarStack` | required `label:`; `size: :md`, `max: nil`, `id: nil` | `span[data-nk=avatar-stack][role=group]`; sizes `xs sm md lg` | `label:` names the group; `aria: { label: }` raises. `avatar(src:, alt:, fallback:, loading:, decoding:, id:, html:, aria:, data:, desperately_need_a_class:)` takes explicit keywords and inherits the stack size. Declarations are collected, so Nitro renders the avatars then the single overflow regardless of declaration order. `max:` bounds the visible avatars and derives a `+N` indicator from the remainder; it cannot be combined with an explicit `overflow(count, label:)`, whose count must be positive and whose accessible label is owned by `label:`. A derived indicator is named from `nitro_kit.avatar_stack.overflow` with the remaining count. |
|
|
33
|
+
| `Badge` | optional text or a content block; `variant: :default`, `size: :md`, `color: :zinc`, `id: nil` | `span[data-nk=badge]`; variants `default outline`; sizes `xs sm md`; Tailwind color palette plus semantic aliases | Content has exactly one path: label text or a content block, never both. Blank text raises at construction, the earliest point it is knowable; missing content raises at render. Both palette vocabularies coexist deliberately, the raw Tailwind family names and the semantic aliases `neutral info success warning danger` that map onto them. `href:` and `dismissible:` are deliberately out of scope; wrap the Badge in a link or pair it with a Button instead. |
|
|
34
|
+
| `Button` | optional text or block; `href: nil`, `variant: :default`, `size: :md`, `icon: nil`, `icon_end: nil`, `label: nil`, `id: nil`, `type: :button`, `name: nil`, `value: nil`, `form: nil`, `target: nil`, `rel: nil`, `download: nil`, `disabled: false`, `loading: false` | native `button` or `a[data-nk=button]`; variants `default primary destructive ghost`; sizes `xs sm md lg xl`; types `button submit reset` | Requires text, a block, or an icon. `default` is the ordinary action treatment; `ghost` is reserved for deliberately low-emphasis interface chrome, not routine secondary actions. Icon-only buttons require `label:`, `aria: { label: }`, or `aria: { labelledby: }`; `label:` and `aria: { label: }` are the same attribute and collide. `icon_end:` matches the `button-icon-end` slot. Blank text, treatment vocabularies, and link/button option mixing raise on construction; text-plus-block and the icon-only accessible name raise at render because only render time knows whether a block supplies the label. `type:` applies to native buttons only and raises when combined with `href:`. `loading: true` disables the control, sets `aria-busy="true"`, and replaces the leading icon with the `button-spinner` slot. Disabled links lose `href`, receive `aria-disabled`, and leave the tab order. |
|
|
35
|
+
| `ButtonTo` | optional text or block; required `href:`; `method: :post`; Button treatment options; root `html:`, `aria:`, `data:`; nested `button_html:`, `button_aria:`, `button_data:` | `form[data-nk=button-to]` containing one slotted submit Button; methods `get post patch put delete` | Uses Rails form semantics, including method emulation and authenticity tokens, for mutations that must not be represented as links. The form root is layout-transparent. Turbo confirmation/frame attributes normally belong on root `data:`; nested trigger attributes such as Tooltip relationships use the `button_*` boundaries. |
|
|
36
|
+
| `ButtonGroup` | `buttons: []`, `label: nil`, `variant: nil`, `size: nil`, `id: nil` | `div[data-nk=button-group][role=group]` | Requires one or more Buttons through `buttons:`, `add`, or `button`. Only `NitroKit::Button` is accepted and the same instance cannot appear twice. `button` forwards its arguments verbatim to `Button.new`, so unknown keywords raise from Button. Group-level `variant:` and `size:` are validated against Button vocabularies and fill in only where a `button` declaration stays silent; instances passed to `buttons:` or `add` keep their own treatment. |
|
|
37
|
+
| `CommandPalette` | required `id:`; `label:`, `placeholder:`, `empty_text:`, `close_label:`, and `shortcut_label:` defaulting to `I18n.t("nitro_kit.command_palette.*")`; `shortcut: true`; `search_url: nil` | `div[data-nk=command-palette]` owning one native dialog and `turbo-frame[data-nk=command-palette-results]` | Requires one or more `destination(label, href:, description: nil, html:, aria:, data:, desperately_need_a_class:)` declarations. The full native link list and declarative dialog trigger work without JavaScript; enhancement reveals filtering, adds translated result announcements, and opens or closes the palette with Command-K or Control-K when `shortcut: true`. `shortcut: false` omits the global shortcut and its visible hint so more than one palette can be composed without conflicting global owners. `search_url:` turns the search region into a debounced GET form whose `query` field targets the owned Turbo Frame. The endpoint returns `NitroKit::CommandPalette::Results.new(id:)` with the same stable ID; zero results are valid. Applications render only authorized destinations and own current-route and search policy. |
|
|
38
|
+
| `Icon` | `name`; `size: :md`, `label: nil`, `stroke_width: 1.5`, `id: nil` | `svg[data-nk=icon]`; sizes `xs sm md lg xl` | Unknown Lucide names raise. `stroke_width:` must be a number between 0.5 and 4. Icons are decorative by default; `label:` produces a labelled image. Owned `aria-label`, `aria-hidden`, and `role` follow the ordinary merge rules, so caller ARIA that collides raises instead of being discarded. |
|
|
39
|
+
| `Pagination` | `label:` defaulting to `I18n.t("nitro_kit.pagination.label")`, `pagy: nil`, `page_url: nil`, `id: nil` | `nav[data-nk=pagination]` | Accepts either a declaration block or a Pagy object. A Pagy object builds previous, series, gap, current, and next items automatically through Pagy's public `previous`, `next`, `data_hash`, and `page_url` API; a callable `page_url:` overrides those destinations. `prev` and `next` fall back to their default labels only when no content block supplies one. The current page without an href renders `span[data-slot=pagination-current][aria-current=page]`, not a Button. Manual items retain ordering, uniqueness, current-link, and ellipsis validation. Default previous/next text, their fallback aria-labels, and the ellipsis label come from `nitro_kit.pagination.*`. |
|
|
40
|
+
|
|
41
|
+
### Forms
|
|
42
|
+
|
|
43
|
+
| Component | Constructor-specific options | Root and closed vocabulary | Contract |
|
|
44
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| `AppearancePicker` | required `id:`; `label:` defaulting to `I18n.t("nitro_kit.appearance_picker.label")`; `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 AppShell identifier contract. Preference names come from `nitro_kit.appearance_picker.preferences.*`. |
|
|
46
|
+
| `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:` to bind `aria-describedby`. 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"`. |
|
|
47
|
+
| `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`. |
|
|
48
|
+
| `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. |
|
|
49
|
+
| `Dropzone` | required `id:`, `name:`; `label:` defaulting to `I18n.t("nitro_kit.dropzone.label")`, `description: nil`, `presentation: :input` (`input minimal`), `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. `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. `presentation: :minimal` hides the native input visually while keeping it focusable and named, so the drop target is the only visible affordance. |
|
|
50
|
+
| `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. |
|
|
51
|
+
| `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. |
|
|
52
|
+
| `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. |
|
|
53
|
+
| `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. |
|
|
54
|
+
| `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. `type: :date` is the only date control; its CSS normalizes Safari's internal date editor alignment, so `Field`/`FormBuilder` `as: :date` inherits the fix. |
|
|
55
|
+
| `Label` | optional text; `for: nil`, `id: nil` | native `label[data-nk=label]` | Requires non-blank text or a content block. |
|
|
56
|
+
| `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. `invalid: true` sets `aria-invalid` on the control. Selection is entirely native: no Stimulus controller and no mirrored `data-state`. |
|
|
57
|
+
| `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`. |
|
|
58
|
+
| `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. |
|
|
59
|
+
| `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 is nested in the content slot beside the label text and 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"`. |
|
|
60
|
+
| `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. |
|
|
61
|
+
|
|
62
|
+
`Field` accepts:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
NitroKit::Field.new(
|
|
66
|
+
form = nil,
|
|
67
|
+
field_name = nil,
|
|
68
|
+
as: :string,
|
|
69
|
+
label: nil,
|
|
70
|
+
description: nil,
|
|
71
|
+
errors: nil,
|
|
72
|
+
options: nil,
|
|
73
|
+
option_tags: nil,
|
|
74
|
+
include_blank: nil,
|
|
75
|
+
prompt: nil,
|
|
76
|
+
id: nil,
|
|
77
|
+
name: nil,
|
|
78
|
+
value: nil, # omitted derives the value from the form object
|
|
79
|
+
placeholder: nil,
|
|
80
|
+
disabled: false,
|
|
81
|
+
readonly: false,
|
|
82
|
+
required: false,
|
|
83
|
+
autocomplete: nil,
|
|
84
|
+
checked: nil,
|
|
85
|
+
indeterminate: false,
|
|
86
|
+
multiple: false,
|
|
87
|
+
accept: nil,
|
|
88
|
+
min: nil,
|
|
89
|
+
max: nil,
|
|
90
|
+
step: nil,
|
|
91
|
+
minlength: nil,
|
|
92
|
+
maxlength: nil,
|
|
93
|
+
rows: nil,
|
|
94
|
+
cols: nil,
|
|
95
|
+
wrap: nil,
|
|
96
|
+
pattern: nil,
|
|
97
|
+
inputmode: nil,
|
|
98
|
+
checked_value: "1",
|
|
99
|
+
unchecked_value: "0",
|
|
100
|
+
include_hidden: true,
|
|
101
|
+
html: {},
|
|
102
|
+
aria: {},
|
|
103
|
+
data: {},
|
|
104
|
+
control_html: {},
|
|
105
|
+
control_aria: {},
|
|
106
|
+
control_data: {},
|
|
107
|
+
desperately_need_a_class: nil
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`rows:`, `cols:`, and `wrap:` require `as: :textarea`. Every control keyword is owned by the composed atom; passing one through `control_html:` raises and names the keyword.
|
|
112
|
+
|
|
113
|
+
Field types are `button color date datetime datetime_local email file hidden month number password range rich_text search string tel text time url week select combobox textarea checkbox radio radio_button radio_group switch`. `string` becomes a text input; `datetime` and `datetime_local` become `datetime-local`. `rich_text` captures the host application's installed `rich_text_area` helper and wraps it in `RichTextArea`.
|
|
114
|
+
|
|
115
|
+
### Structured content and interaction
|
|
116
|
+
|
|
117
|
+
| Component | Constructor-specific options | Root and closed vocabulary | Compound contract |
|
|
118
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
119
|
+
| `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`. 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. |
|
|
120
|
+
| `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`. |
|
|
121
|
+
| `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. |
|
|
122
|
+
| `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`. 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). |
|
|
123
|
+
| `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. `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`. |
|
|
124
|
+
| `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 AppShell 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 without mirroring state, and returns focus to the trigger whenever the popover closes with focus still inside it. |
|
|
125
|
+
| `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. |
|
|
126
|
+
| `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)`. 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. |
|
|
127
|
+
| `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:` and `direction:` are both set or both nil and mirror onto the root as `data-sort` and `data-direction`. `table_html:`, `table_aria:`, and `table_data:` address the inner `table` element. |
|
|
128
|
+
| `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, hiding, roving focus, and keyboard activation after enhancement. |
|
|
129
|
+
| `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, so server-rendered flash items are announced without waiting for a DOM mutation. 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`. |
|
|
130
|
+
| `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. `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 and the controller only adds Escape dismissal and reset. |
|
|
131
|
+
| `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. |
|
|
132
|
+
|
|
133
|
+
## Layout primitives
|
|
134
|
+
|
|
135
|
+
Only closed, component-specific layout values are public. `VStack` and `HStack` have been removed; choose an explicit `Flex` direction instead.
|
|
136
|
+
|
|
137
|
+
| Layout | Constructor | Root | Contract |
|
|
138
|
+
| ----------- | ----------------------------------------------------------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
139
|
+
| `Flex` | required `dir:`; `gap: 4`, `align: :start`, `justify: :start`, `wrap: :nowrap`, `id: nil` | `div[data-nk=flex]` | Direction, gap, alignment, justification, and wrapping accept scalar or responsive values. Direct content; empty content is valid. |
|
|
140
|
+
| `Grid` | required `cols:`; `gap: 4`, `id: nil` | `div[data-nk=grid]` | Columns and gap accept scalar or responsive values. Equal `minmax(0, 1fr)` tracks; direct content; empty content is valid. |
|
|
141
|
+
| `Container` | required `size:`, `id: nil` | `div[data-nk=container]` | Sizes `sm md lg xl`; centered maximum width. Omit the component for full-width content. |
|
|
142
|
+
|
|
143
|
+
Responsive properties use a property-scoped whitespace-separated string:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
render NitroKit::Flex.new(
|
|
147
|
+
dir: "col md:row",
|
|
148
|
+
gap: "3 md:6",
|
|
149
|
+
align: "stretch md:center",
|
|
150
|
+
justify: "start md:between",
|
|
151
|
+
wrap: "nowrap lg:wrap"
|
|
152
|
+
) do
|
|
153
|
+
render WorkspaceSummary.new
|
|
154
|
+
render WorkspaceActions.new
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
render NitroKit::Grid.new(cols: "1 sm:2 lg:3", gap: "3 lg:6") do
|
|
158
|
+
records.each { |record| render RecordCard.new(record) }
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The grammar is `BASE sm:VALUE md:VALUE lg:VALUE xl:VALUE 2xl:VALUE`. A base token is required and applies below the first override. Prefixes are fixed minimum widths: `sm` 40rem, `md` 48rem, `lg` 64rem, `xl` 80rem, and `2xl` 96rem. Prefix tokens may be omitted, but a prefix and the base may each appear at most once.
|
|
163
|
+
|
|
164
|
+
Responsive strings and rendered attributes use hyphenated tokens such as `row-reverse`, `col-reverse`, and `wrap-reverse`. Their idiomatic scalar Ruby forms are `:row_reverse`, `:col_reverse`, and `:wrap_reverse`; Nitro normalizes those symbols to the hyphenated data values. Every accepted value is normalized to base then `sm md lg xl 2xl` order and mirrored in the corresponding owned data attribute:
|
|
165
|
+
|
|
166
|
+
```html
|
|
167
|
+
<div
|
|
168
|
+
data-nk="flex"
|
|
169
|
+
data-dir="col md:row"
|
|
170
|
+
data-gap="3 md:6"
|
|
171
|
+
data-align="stretch md:center"
|
|
172
|
+
data-justify="start md:between"
|
|
173
|
+
data-wrap="nowrap lg:wrap"
|
|
174
|
+
></div>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
| Property | Closed values |
|
|
178
|
+
| -------------- | --------------------------------------------------------------- |
|
|
179
|
+
| Flex `dir` | `row col row-reverse col-reverse` |
|
|
180
|
+
| Flex `align` | `start center end stretch baseline` |
|
|
181
|
+
| Flex `justify` | `start center end between around evenly` |
|
|
182
|
+
| Flex `wrap` | `nowrap wrap wrap-reverse` |
|
|
183
|
+
| Grid `cols` | integers `1..12` |
|
|
184
|
+
| Shared `gap` | integers `0 1 2 3 4 5 6 8 10 12 16`, multiplied by `--nk-space` |
|
|
185
|
+
|
|
186
|
+
Unknown values or prefixes, duplicate base/prefix tokens, missing base values, blank strings, and non-scalar/non-string inputs raise `ArgumentError`. The grammar belongs to one property at a time; Nitro does not accept a Tailwind class list, arbitrary/custom breakpoints, max/range/container prefixes, or arbitrary CSS values.
|
|
187
|
+
|
|
188
|
+
## Application layout and page sections
|
|
189
|
+
|
|
190
|
+
| Component | Constructor | Root | Compound contract |
|
|
191
|
+
| ---------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
192
|
+
| `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. |
|
|
193
|
+
| `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 hybrid`; 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. |
|
|
194
|
+
| `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)` and renders `nav > ul > li > a`, where `icon:` names a Lucide icon; a current item uses `aria-current="page"` and at most one item is current. Routes remain caller-owned. |
|
|
195
|
+
| `Toolbar` | `id: nil` | `div[data-nk=toolbar]` | At most one `leading` and one `trailing`; at least one region total. It deliberately has no toolbar role, sticky mode, or action registry. |
|
|
196
|
+
| `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. |
|
|
197
|
+
| `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`. |
|
|
198
|
+
| `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. |
|
|
199
|
+
| `DataSection` | `title: nil`, `description: nil`, `id: nil` | `section[data-nk=data-section]` | Requires `title`; `title` and `description` each accept constructor text or one matching text/rich-content compound declaration. Exactly one `table(Table | DetailsTable)`or level-3`empty_state(EmptyState)`; at most one `Button`or`ButtonGroup`through`actions`. The shadowed Phlex element remains available as `html_table`. |
|
|
200
|
+
| `FormSection` | `title: nil`, `description: nil`, `id: nil` | `section[data-nk=form-section][aria-labelledby]` naming the title heading | Requires `title`; `title` and `description` each accept constructor text or one matching text/rich-content compound declaration. Exactly one `form` block; at most one `Alert` through `status`. |
|
|
201
|
+
| `DangerZone` | `title: nil`, `description: nil`, `id: nil` | `section[data-nk=danger-zone]` | 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. |
|
|
202
|
+
| `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. |
|
|
203
|
+
|
|
204
|
+
## Non-visual Rails integration
|
|
205
|
+
|
|
206
|
+
`NitroKit::AppearanceBootstrap.new(default: :system, nonce: nil)` renders a non-visual head script. Defaults are `light`, `dark`, and `system`. Its fixed hashable body reads the default from script data, installs one idempotent document runtime before stylesheets, tolerates denied or malformed storage, and sets the preference plus resolved `data-theme="light|dark"`. See [Rails and Hotwire integration](rails_integration.md#appearance-and-content-security-policy) for nonce and stable-hash setup.
|
|
207
|
+
|
|
208
|
+
`NitroKit::Choice` is a validated value object with `label:`, `value:`, `disabled: false`, `id: nil`, and `description: nil`. `Choice.coerce` accepts an existing Choice, a matching Hash, a `[label]` or `[label, value]` array, or a scalar used for both label and value. Longer arrays raise; `disabled`, `id`, and `description` require the Hash form. `Combobox::Option` is removed.
|
|
209
|
+
|
|
210
|
+
`NitroKit::FormBuilder` is selected explicitly through Rails `form_with`. Its Nitro methods are:
|
|
211
|
+
|
|
212
|
+
- `field`, `fieldset`, and `group`.
|
|
213
|
+
- `dropzone` for native file selection with optional Active Storage direct uploads.
|
|
214
|
+
- `select`, `radio_button`, `check_box`/`checkbox`, and `hidden_field`.
|
|
215
|
+
- `field(:attribute, as: :combobox, options: ...)` for the searchable control inside an ordinary Field.
|
|
216
|
+
- `submit` and `button`.
|
|
217
|
+
- Rails-shaped color, date, datetime, email, file, month, number, password, phone/telephone, range, search, text, textarea, time, URL, and week field methods.
|
|
218
|
+
|
|
219
|
+
Every builder method uses one attribute boundary: `html:`, `aria:`, and `data:`
|
|
220
|
+
decorate the control, `control_html:`, `control_aria:`, and `control_data:` are
|
|
221
|
+
their explicit long forms, and the same key in both forms raises. The Field
|
|
222
|
+
wrapper is reachable through `wrapper_html:`, `wrapper_aria:`, and
|
|
223
|
+
`wrapper_data:`. `as:` is validated at the builder layer. Labels come from
|
|
224
|
+
`human_attribute_name` when the form object supplies one. `submit` renders a
|
|
225
|
+
primary submit Button named `commit` whose default text is
|
|
226
|
+
`I18n.t("nitro_kit.form.submit")`, `dropzone` forwards `label:` to `Dropzone`,
|
|
227
|
+
`button` defaults to `type: :submit`, and
|
|
228
|
+
the Rails helpers Nitro does not style — `label`, `collection_select`,
|
|
229
|
+
`grouped_collection_select`, `collection_radio_buttons`,
|
|
230
|
+
`collection_check_boxes`, `date_select`, and `time_zone_select` — raise and name
|
|
231
|
+
their `form.field(as:)` equivalent.
|
|
232
|
+
|
|
233
|
+
It has no Nitro root and does not provide `nk_form_with`, `nk_form_for`, or an ERB component bridge. See [Rails and Hotwire integration](rails_integration.md).
|
|
234
|
+
|
|
235
|
+
For a standalone form, put its visible controls and in-form actions inside
|
|
236
|
+
`form.group`. Hidden fields may remain before the group. A shell form whose
|
|
237
|
+
only submit lives in the toolbar still groups its visible fields in the form
|
|
238
|
+
body and associates the external Button through the native `form` attribute.
|
|
239
|
+
|
|
240
|
+
## Behavior and dependency boundaries
|
|
241
|
+
|
|
242
|
+
| Surface | Nitro behavior | Host dependency |
|
|
243
|
+
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
244
|
+
| Appearance | The bootstrap singleton restores and persists preference, resolves theme, owns media/storage listeners, and broadcasts updates even without a picker. Pickers only request and synchronize changes and release their subscriptions. | Browser storage and `matchMedia`; Stimulus for pickers. Storage failure falls back without raising |
|
|
245
|
+
| App shell | A native dialog owns narrow-modal focus containment, background inertness, Escape, and the top layer. `nk--app-shell` moves one navigation subtree between desktop and dialog, coordinates breakpoints/trigger state, restores focus, handles backdrop/Turbo closes, and restores the no-JavaScript tree on teardown. | Stimulus and Turbo events; no navigation library |
|
|
246
|
+
| Avatar | `nk--avatar` marks the root `data-state="error"` when the image fails to load so CSS reveals the initials fallback instead of a broken-image glyph | Stimulus only for the image error hook |
|
|
247
|
+
| Combobox | A native select remains the named no-JavaScript control. `nk--combobox` progressively adds searchable listbox filtering, APG focus/keyboard state, validation, announcements, and restores the native control on disconnect. Every submitted-value change, including clearing it while filtering, dispatches one bubbling `change` event. Its announcement strings come from the `nitro_kit.combobox.*` locale scope and reach the controller as Stimulus values. | Stimulus; no combobox or positioning package |
|
|
248
|
+
| Command palette | Native dialog commands and destination links remain the baseline. `nk--command-palette` adds the optional Command-K/Control-K shortcut, reveals and filters the search input, announces result counts, preserves native link navigation, resets on close, and closes without stale state before Turbo visits or caching. | Stimulus for search and the global shortcut; no command-menu package |
|
|
249
|
+
| Checkbox | Checked state is native. `nk--checkable` mounts only for `indeterminate: true`, because that DOM property has no markup form; it applies the property and owns `data-state="indeterminate"`. Switch and RadioButton mount nothing at all. | Stimulus only for the indeterminate property |
|
|
250
|
+
| Accordion | Native `details`/`summary` owns disclosure; one shared `name` gives single mode its exclusivity | No JavaScript |
|
|
251
|
+
| Dialog | Native declarative commands open and close the dialog and the browser owns modal focus and Escape; `nk--dialog` adds backdrop light dismissal and blocks Escape for `dismissible: false` panels | Stimulus only for light dismissal and cancel suppression |
|
|
252
|
+
| Dropdown | Native Popover owns visibility and implicit invoker state; `nk--dropdown` only adds APG-style initial focus, arrow/Home/End navigation, selection closing, and focus restoration to the trigger whenever the popover closes with focus inside it | Stimulus for the keyboard enhancement; CSS anchor positioning with a centered viewport fallback |
|
|
253
|
+
| Tooltip | CSS owns hover and focus visibility; `nk--tooltip` suppresses the visible tooltip on Escape until hover/focus interest ends | Stimulus only for Escape dismissal |
|
|
254
|
+
| Tabs | Server markup exposes every panel. `nk--tabs` adds the APG single-panel view, roving focus, automatic/manual activation, and restores every panel on disconnect. | Stimulus; no tabs package |
|
|
255
|
+
| Toast | `nk--toast` owns dismiss/pause/resume timers, keeps overlapping hover/focus interest paused, removes closed items before caching, and clears every timer on disconnect. Every item is Turbo-temporary; the addressable region and list are not. | Stimulus timers; application owns adding server-rendered items |
|
|
256
|
+
| Details table | No Nitro JavaScript | Rails localization/routes; Active Record and Active Storage values when the caller supplies them |
|
|
257
|
+
| Progressive image | `nk--progressive-image` reflects decode/load/error state without class mutation, rebinds replaced targets, invalidates pending decodes, and removes exact image listeners. | Active Storage variants and the host's configured image processor |
|
|
258
|
+
| Dropzone | `nk--dropzone` owns drag/selection preview and state while focused modules own file policy, shared form locks, and abortable Active Storage requests. Disconnect preserves selected native files; Turbo cache teardown clears transient state. | Native file submission fallback plus optional Active Storage direct uploads; no Dropzone.js |
|
|
259
|
+
| Sortable table | No Nitro JavaScript; application owns query and sorting policy | None. Ransack appears only in an optional gallery recipe and is not a runtime dependency |
|
|
260
|
+
|
|
261
|
+
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.
|
|
262
|
+
|
|
263
|
+
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.
|