nitro_kit 2.0.0.alpha.2 → 2.0.0.alpha.3
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 +50 -1
- data/README.md +23 -4
- data/STYLE_GUIDE.md +37 -5
- data/app/assets/stylesheets/nitro_kit.css +1714 -1358
- data/app/components/nitro_kit/accordion.rb +1 -7
- data/app/components/nitro_kit/app_navigation.rb +1 -2
- data/app/components/nitro_kit/app_shell.rb +1 -7
- data/app/components/nitro_kit/appearance_picker.rb +1 -11
- data/app/components/nitro_kit/avatar.rb +6 -1
- data/app/components/nitro_kit/avatar_stack.rb +3 -0
- data/app/components/nitro_kit/button_group.rb +2 -2
- data/app/components/nitro_kit/checkbox.rb +7 -4
- data/app/components/nitro_kit/checkbox_group.rb +0 -25
- data/app/components/nitro_kit/combobox.rb +3 -16
- data/app/components/nitro_kit/command_palette.rb +41 -59
- data/app/components/nitro_kit/component.rb +57 -5
- data/app/components/nitro_kit/danger_zone.rb +5 -1
- data/app/components/nitro_kit/data_section.rb +28 -4
- data/app/components/nitro_kit/details_table.rb +19 -8
- data/app/components/nitro_kit/dialog.rb +23 -19
- data/app/components/nitro_kit/dropdown.rb +1 -6
- data/app/components/nitro_kit/dropzone.rb +3 -14
- data/app/components/nitro_kit/empty_state.rb +21 -2
- data/app/components/nitro_kit/field.rb +35 -11
- data/app/components/nitro_kit/form_builder.rb +0 -1
- data/app/components/nitro_kit/progressive_image.rb +2 -2
- data/app/components/nitro_kit/radio_button.rb +7 -4
- data/app/components/nitro_kit/radio_button_group.rb +0 -25
- data/app/components/nitro_kit/select.rb +3 -3
- data/app/components/nitro_kit/settings_layout.rb +17 -4
- data/app/components/nitro_kit/{form_section.rb → settings_section.rb} +18 -11
- data/app/components/nitro_kit/sheet.rb +29 -20
- data/app/components/nitro_kit/stat_grid.rb +18 -2
- data/app/components/nitro_kit/table.rb +22 -3
- data/app/components/nitro_kit/tabs.rb +1 -13
- data/app/components/nitro_kit/toast.rb +4 -2
- data/app/components/nitro_kit/toolbar.rb +2 -0
- data/app/components/nitro_kit/tooltip.rb +7 -10
- data/app/javascript/controllers/nk/app_shell_controller.js +3 -3
- data/app/javascript/controllers/nk/appearance_controller.js +6 -0
- data/app/javascript/controllers/nk/combobox_controller.js +51 -1
- data/app/javascript/controllers/nk/command_palette_controller.js +21 -11
- data/app/javascript/controllers/nk/dialog_controller.js +54 -0
- data/app/javascript/controllers/nk/dropdown_controller.js +45 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +2 -2
- data/app/javascript/controllers/nk/progressive_image_controller.js +11 -7
- data/app/javascript/controllers/nk/tabs_controller.js +8 -1
- data/app/javascript/controllers/nk/tooltip_controller.js +9 -0
- data/config/locales/en.yml +4 -1
- data/docs/agent_guide.md +31 -16
- data/docs/agent_native_spec.md +27 -10
- data/docs/browser_support.md +193 -0
- data/docs/component_contracts.md +88 -82
- data/docs/customization.md +27 -15
- data/docs/hotwire.md +15 -3
- data/docs/initialization_prompt.md +31 -6
- data/docs/migration_1_to_2.md +61 -19
- data/docs/patterns/application_foundation.md +6 -4
- data/docs/patterns/crud_resource.md +6 -3
- data/docs/patterns/destructive_action.md +25 -0
- data/docs/patterns/resource_form.md +1 -1
- data/docs/rails_conventions.md +4 -1
- data/docs/rails_integration.md +39 -11
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +12 -0
- data/lib/nitro_kit/installation.rb +9 -4
- data/lib/nitro_kit/migration_inventory.rb +268 -8
- data/lib/nitro_kit/upgrade_smoke_test.rb +10 -12
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +10 -3
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +6 -1
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +18 -8
- data/src/stylesheets/nitro_kit/components/accordion.css +40 -40
- data/src/stylesheets/nitro_kit/components/alert.css +9 -9
- data/src/stylesheets/nitro_kit/components/app_navigation.css +19 -19
- data/src/stylesheets/nitro_kit/components/app_shell.css +196 -193
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +41 -31
- data/src/stylesheets/nitro_kit/components/avatar.css +3 -3
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +36 -19
- data/src/stylesheets/nitro_kit/components/button.css +83 -58
- data/src/stylesheets/nitro_kit/components/button_group.css +19 -14
- data/src/stylesheets/nitro_kit/components/card.css +11 -5
- data/src/stylesheets/nitro_kit/components/checkbox.css +40 -36
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +13 -13
- data/src/stylesheets/nitro_kit/components/combobox.css +33 -23
- data/src/stylesheets/nitro_kit/components/command_palette.css +48 -47
- data/src/stylesheets/nitro_kit/components/control_group.css +52 -58
- data/src/stylesheets/nitro_kit/components/danger_zone.css +3 -3
- data/src/stylesheets/nitro_kit/components/data_section.css +1 -1
- data/src/stylesheets/nitro_kit/components/details_table.css +13 -13
- data/src/stylesheets/nitro_kit/components/dialog.css +6 -3
- data/src/stylesheets/nitro_kit/components/dropdown.css +29 -23
- data/src/stylesheets/nitro_kit/components/dropzone.css +71 -28
- data/src/stylesheets/nitro_kit/components/empty_state.css +6 -6
- data/src/stylesheets/nitro_kit/components/field.css +30 -26
- data/src/stylesheets/nitro_kit/components/field_group.css +13 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +6 -0
- data/src/stylesheets/nitro_kit/components/input.css +9 -2
- data/src/stylesheets/nitro_kit/components/layout.css +165 -165
- data/src/stylesheets/nitro_kit/components/page_header.css +3 -3
- data/src/stylesheets/nitro_kit/components/pagination.css +55 -46
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +7 -7
- data/src/stylesheets/nitro_kit/components/palette.css +20 -20
- data/src/stylesheets/nitro_kit/components/progressive_image.css +44 -54
- data/src/stylesheets/nitro_kit/components/radio_button.css +36 -29
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +43 -43
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +15 -0
- data/src/stylesheets/nitro_kit/components/select.css +2 -9
- data/src/stylesheets/nitro_kit/components/settings_layout.css +21 -18
- data/src/stylesheets/nitro_kit/components/settings_section.css +84 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +23 -3
- data/src/stylesheets/nitro_kit/components/stat_grid.css +10 -9
- data/src/stylesheets/nitro_kit/components/switch.css +37 -33
- data/src/stylesheets/nitro_kit/components/table.css +35 -31
- data/src/stylesheets/nitro_kit/components/tabs.css +43 -39
- data/src/stylesheets/nitro_kit/components/toast.css +20 -9
- data/src/stylesheets/nitro_kit/components/toolbar.css +14 -39
- data/src/stylesheets/nitro_kit/components/tooltip.css +34 -29
- data/src/stylesheets/nitro_kit/components/typeset.css +147 -1
- data/src/stylesheets/nitro_kit/reset.css +9 -9
- data/src/stylesheets/nitro_kit/tokens.css +8 -9
- metadata +4 -3
- data/src/stylesheets/nitro_kit/components/form_section.css +0 -71
data/docs/component_contracts.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Nitro Kit 2.0 component contracts
|
|
2
2
|
|
|
3
|
-
This is the shipped public catalog for `2.0.0.alpha.
|
|
3
|
+
This is the shipped public catalog for `2.0.0.alpha.3`. 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
4
|
|
|
5
5
|
## Shared component boundary
|
|
6
6
|
|
|
@@ -12,9 +12,9 @@ html: {}, aria: {}, data: {}, desperately_need_a_class: nil
|
|
|
12
12
|
|
|
13
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
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`,
|
|
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`, `data-field-type`, `data-dir`, `data-gap`, `data-align`, `data-justify`, `data-wrap`, `data-cols`, `data-mode`, and `data-key`. 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
16
|
|
|
17
|
-
The single styling escape is `desperately_need_a_class:`. It
|
|
17
|
+
The single styling escape is `desperately_need_a_class:`. It accepts the same strings, symbols, nested arrays, and conditional hashes as Rails `class_names`, normalizes them to a deduplicated string, and emits both the class and `data-nk-escape="class"`. The normalized value must contain at least one class name. There is no untyped structural bypass. Tailwind applications may additionally use an application-owned Tailwind merger when conflicting utility classes need resolution; Nitro itself does not depend on Tailwind.
|
|
18
18
|
|
|
19
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
20
|
|
|
@@ -24,40 +24,40 @@ Every default string a person can read or hear comes from the engine-loaded `nit
|
|
|
24
24
|
|
|
25
25
|
### Actions, display, and navigation
|
|
26
26
|
|
|
27
|
-
| Component
|
|
28
|
-
|
|
|
29
|
-
| `Alert`
|
|
30
|
-
| `AppNavigation`
|
|
31
|
-
| `Avatar`
|
|
32
|
-
| `AvatarStack`
|
|
33
|
-
| `Badge`
|
|
34
|
-
| `Button`
|
|
35
|
-
| `ButtonTo`
|
|
36
|
-
| `ButtonGroup`
|
|
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`
|
|
38
|
-
| `Icon`
|
|
39
|
-
| `Pagination`
|
|
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`, `decorative: false`, `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. An image with empty `alt:` raises unless `decorative: true`. 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:, decorative:, 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:`. The indicator carries `role="img"`; a derived one 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`, `submission_indicator: nil` | 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. `submission_indicator: :spinner` applies only to native submit Buttons, cannot combine with `loading:`, and renders the `button-submission-spinner` slot that `nk--button` reveals during a slow Turbo submission. |
|
|
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. `label:` and `aria: { label: }` are the same attribute and collide. |
|
|
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
40
|
|
|
41
41
|
### Forms
|
|
42
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`
|
|
46
|
-
| `Checkbox`
|
|
47
|
-
| `CheckboxGroup`
|
|
48
|
-
| `ControlGroup`
|
|
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`
|
|
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`
|
|
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. `
|
|
55
|
-
| `Label` | optional text; `for: nil`, `id: nil` | native `label[data-nk=label]` | Requires non-blank text or a content block.
|
|
56
|
-
| `RadioButton`
|
|
57
|
-
| `RadioButtonGroup` | required `legend:`, `options:`, `name:`; `value: nil`, `id: nil`, `description: nil`, `orientation: :vertical`, `presentation: :list`, `disabled: false`, `required: false`, `size: :md`
|
|
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`
|
|
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.
|
|
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 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.*`. |
|
|
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:`, 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"`. |
|
|
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. A sibling immediately following the group receives the group gap as `margin-block-start`. |
|
|
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. `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. |
|
|
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 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`. |
|
|
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 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"`. |
|
|
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
61
|
|
|
62
62
|
`Field` accepts:
|
|
63
63
|
|
|
@@ -110,25 +110,25 @@ NitroKit::Field.new(
|
|
|
110
110
|
|
|
111
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
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`.
|
|
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`. `month` and `week` preserve those native input types but add no cross-browser normalization or picker; their ISO format, range, and step rules remain server responsibilities. Use `as: :select` with explicit application-owned options when exact selection matters. `rich_text` captures the host application's installed `rich_text_area` helper and wraps it in `RichTextArea`.
|
|
114
114
|
|
|
115
115
|
### Structured content and interaction
|
|
116
116
|
|
|
117
|
-
| Component | Constructor-specific options | Root and closed vocabulary
|
|
118
|
-
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
119
|
-
| `Accordion` | required `id:`; `mode: :multiple` | `div[data-nk=accordion]`; modes `multiple single`
|
|
120
|
-
| `Card` | `id: nil` | `article[data-nk=card]`; title levels `1..6`
|
|
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`
|
|
122
|
-
| `Dialog` | required `id:`; `dismissible: true`
|
|
123
|
-
| `DetailsTable` | `record`, `caption: nil`, `label: nil`, `empty_text: nil`, `boolean_labels: nil`, `route_base: nil`, `id: nil`
|
|
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
|
|
125
|
-
| `ProgressiveImage` | required `attachment:`; `alt: nil`, `size: :md`, `decorative: false`, `id: nil`
|
|
126
|
-
| `Sheet` | required `id:`; `side: :right`, `size: :md`, `close_label:`
|
|
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
|
|
128
|
-
| `Tabs` | required `id:`; `default: nil`, `label:` defaulting to `I18n.t("nitro_kit.tabs.label")`, `orientation: :horizontal`, `activation: :automatic`
|
|
129
|
-
| `Toast` | `duration: 5000`, `label:` defaulting to `I18n.t("nitro_kit.toast.label")`, `id: "nk-toast"`
|
|
130
|
-
| `Tooltip` | required `id:`, `content:`; `placement: :top` | `span[data-nk=tooltip]`; placements `top right bottom left`
|
|
131
|
-
| `Typeset` | `id: nil` | `div[data-nk=typeset]`
|
|
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`, 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). |
|
|
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, 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`. |
|
|
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 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. |
|
|
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)`; 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. |
|
|
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:` 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. |
|
|
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, 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`. |
|
|
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; 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. |
|
|
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. 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. |
|
|
132
132
|
|
|
133
133
|
## Layout primitives
|
|
134
134
|
|
|
@@ -187,19 +187,19 @@ Unknown values or prefixes, duplicate base/prefix tokens, missing base values, b
|
|
|
187
187
|
|
|
188
188
|
## Application layout and page sections
|
|
189
189
|
|
|
190
|
-
| Component
|
|
191
|
-
|
|
|
192
|
-
| `AuthShell`
|
|
193
|
-
| `AppShell`
|
|
194
|
-
| `SettingsLayout`
|
|
195
|
-
| `Toolbar`
|
|
196
|
-
| `PaginationBar`
|
|
197
|
-
| `PageHeader`
|
|
198
|
-
| `StatGrid`
|
|
199
|
-
| `DataSection`
|
|
200
|
-
| `
|
|
201
|
-
| `DangerZone`
|
|
202
|
-
| `EmptyState`
|
|
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, 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. |
|
|
195
|
+
| `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. |
|
|
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. Stats are declared inside the render block; the root spans the full inline size. |
|
|
199
|
+
| `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 | 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
|
+
| `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`. |
|
|
201
|
+
| `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. |
|
|
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. Declaring a compound method outside the render block raises. |
|
|
203
203
|
|
|
204
204
|
## Non-visual Rails integration
|
|
205
205
|
|
|
@@ -214,7 +214,7 @@ Unknown values or prefixes, duplicate base/prefix tokens, missing base values, b
|
|
|
214
214
|
- `select`, `radio_button`, `check_box`/`checkbox`, and `hidden_field`.
|
|
215
215
|
- `field(:attribute, as: :combobox, options: ...)` for the searchable control inside an ordinary Field.
|
|
216
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.
|
|
217
|
+
- Rails-shaped color, date, datetime, email, file, month, number, password, phone/telephone, range, search, text, textarea, time, URL, and week field methods. `month_field` and `week_field` emit the corresponding native types as progressive enhancement and do not replace server-side ISO format, range, or step validation.
|
|
218
218
|
|
|
219
219
|
Every builder method uses one attribute boundary: `html:`, `aria:`, and `data:`
|
|
220
220
|
decorate the control, `control_html:`, `control_aria:`, and `control_data:` are
|
|
@@ -239,24 +239,30 @@ body and associates the external Button through the native `form` attribute.
|
|
|
239
239
|
|
|
240
240
|
## Behavior and dependency boundaries
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
|
248
|
-
|
|
|
249
|
-
|
|
|
250
|
-
|
|
|
251
|
-
|
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
|
|
|
255
|
-
|
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
|
|
|
259
|
-
|
|
|
242
|
+
This table assigns ownership and dependencies. The canonical dated browser
|
|
243
|
+
matrix and full/reduced/unavailable no-JavaScript classifications live in
|
|
244
|
+
[`browser_support.md`](browser_support.md); do not infer a JavaScript-free
|
|
245
|
+
interaction merely from server-rendered markup in this table.
|
|
246
|
+
|
|
247
|
+
| Surface | Nitro behavior | Host dependency |
|
|
248
|
+
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
249
|
+
| 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 |
|
|
250
|
+
| 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 |
|
|
251
|
+
| 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 |
|
|
252
|
+
| Button | `nk--button` mounts for `submission_indicator:` or `data-turbo-submits-with`, marks the submitting button `data-state="submitting"` with `aria-busy`, reveals the `button-submission-spinner` slot one second into a Turbo submission, and resets on submit end and before Turbo caches the page. | Stimulus and Turbo submission events |
|
|
253
|
+
| 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. A filter with no matches shows a presentational `combobox-empty` item from `nitro_kit.combobox.no_results`; outside dismissal is a capture-phase document pointerdown listener held only while open. Its announcement strings come from the `nitro_kit.combobox.*` locale scope and reach the controller as Stimulus values. | Stimulus; no combobox or positioning package |
|
|
254
|
+
| Command palette | `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. It shares Dialog's capability-detected Invoker Commands fallback. Without JavaScript, its dialog trigger and destination links are available only where Invoker Commands are supported. | Stimulus for search, the global shortcut, and the compatibility bridge; no command-menu package |
|
|
255
|
+
| 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 |
|
|
256
|
+
| Accordion | Native `details`/`summary` owns disclosure. The no-JavaScript baseline is full for each item opening and closing; in `single` mode a shared `name` provides full exclusive grouping where named details are supported, and a reduced independently-open baseline where they are not. Nitro adds neither a controller nor browser detection for that gap. | No JavaScript |
|
|
257
|
+
| Dialog | Native declarative commands are the preferred open/close path and the browser owns modal focus and Escape. `nk--dialog` delegates command controls, checks their reflected `commandForElement` relationship, and calls only `showModal()` or `close()` when that relationship cannot run; it does not mirror open state or polyfill commands generally. It also adds backdrop light dismissal, blocks Escape for `dismissible: false` panels where `closedby` is ignored, and closes ephemeral panels before Turbo caches the page. | Stimulus for dismissal policy and the focused compatibility bridge |
|
|
258
|
+
| Dropdown | Native Popover owns visibility and implicit invoker state; `nk--dropdown` adds APG-style initial focus, arrow/Home/End navigation, selection closing, focus restoration, collision-aware JavaScript positioning, and an outside-pointer fallback for Popover engines whose light dismissal fails. Without Nitro JavaScript, native Popover visibility remains available but placement is a bounded top-left baseline rather than trigger-relative collision-aware placement. | Stimulus for keyboard, positioning, and compatibility behavior; no menu package |
|
|
259
|
+
| Tooltip | CSS owns hover and focus visibility; `nk--tooltip` suppresses the visible tooltip on Escape until hover/focus interest ends, acting only while the tooltip is shown | Stimulus only for Escape dismissal |
|
|
260
|
+
| 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 |
|
|
261
|
+
| 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 |
|
|
262
|
+
| Details table | No Nitro JavaScript | Rails localization/routes; Active Record and Active Storage values when the caller supplies them |
|
|
263
|
+
| 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 |
|
|
264
|
+
| 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 |
|
|
265
|
+
| 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
266
|
|
|
261
267
|
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
268
|
|