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,422 @@
|
|
|
1
|
+
# Nitro Kit 2.0 — agent-native Phlex UI system
|
|
2
|
+
|
|
3
|
+
This is the canonical architecture specification for the stable Nitro Kit 2.0 line. `tk` is the source of truth for live work status, and [`component_contracts.md`](component_contracts.md) records the shipped Ruby and integration contracts.
|
|
4
|
+
|
|
5
|
+
Nitro Kit is a gem-owned, versioned UI system for Rails. Developers and coding agents compose application interfaces in Ruby with Phlex. Nitro Kit owns component behavior, rendered structure, and default aesthetics. Applications own product code and documented theme overrides, not copies of Nitro Kit internals.
|
|
6
|
+
|
|
7
|
+
## Product contract
|
|
8
|
+
|
|
9
|
+
### Nitro Kit owns the system
|
|
10
|
+
|
|
11
|
+
Nitro Kit owns and versions:
|
|
12
|
+
|
|
13
|
+
- Component Ruby and public initializer and compound-method APIs.
|
|
14
|
+
- Rendered `data-nk`, `data-slot`, state, and ARIA contracts.
|
|
15
|
+
- Static component CSS, the default themes, and public `--nk-*` tokens.
|
|
16
|
+
- Light, dark, and system appearance selection and persistence.
|
|
17
|
+
- Stimulus behavior and Rails engine integration.
|
|
18
|
+
- Layout primitives, blocks, application shells, canonical examples, and tests.
|
|
19
|
+
|
|
20
|
+
Applications may:
|
|
21
|
+
|
|
22
|
+
- Override documented `--nk-*` custom properties.
|
|
23
|
+
- Select a Nitro-owned light, dark, or system appearance.
|
|
24
|
+
- Compose Nitro components into application-specific components.
|
|
25
|
+
- Subclass a component when composition is insufficient, accepting that private methods and internal records are not API.
|
|
26
|
+
- Use `desperately_need_a_class:` when an external integration genuinely requires a class.
|
|
27
|
+
|
|
28
|
+
Core components load from the gem. Generated-copy installation and the promise that applications should edit Nitro source are retired.
|
|
29
|
+
|
|
30
|
+
### Phlex is the composition language
|
|
31
|
+
|
|
32
|
+
Direct component construction is the canonical API:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
render NitroKit::Button.new("Save", variant: :primary)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Compound components expose ordinary Ruby methods:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
render NitroKit::Card.new do |card|
|
|
42
|
+
card.title("Workspace")
|
|
43
|
+
card.body { render WorkspaceSummary.new }
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Fixed-order page blocks accept their textual regions in either concise constructor form or deferred compound form:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
render NitroKit::EmptyState.new(level: 3) do |empty|
|
|
51
|
+
empty.title { plain "No records for "; strong { "Production" } }
|
|
52
|
+
empty.description("Remove one or more filters and try again.")
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The two forms are mutually exclusive for each region. Required content may be satisfied by either form, and deferred blocks may render arbitrary Phlex content without changing the component-owned DOM order.
|
|
57
|
+
|
|
58
|
+
Nitro Kit does not provide `nk_*` ERB helpers, generated variant helpers, `from_template`, conditional builder capture, or general template-buffer bridges.
|
|
59
|
+
|
|
60
|
+
Rails remains first-class where Rails owns meaningful semantics:
|
|
61
|
+
|
|
62
|
+
- Model-backed forms and `ActionView::Helpers::FormBuilder` behavior.
|
|
63
|
+
- Route and URL generation.
|
|
64
|
+
- DOM IDs, translations, assets, and Active Storage.
|
|
65
|
+
- Turbo Frames, Turbo Streams, and related Hotwire helpers.
|
|
66
|
+
|
|
67
|
+
Applications include the `Phlex::Rails::Helpers::*` adapters they actually use. Nitro Kit does not recreate Rails helpers under an `nk_*` namespace.
|
|
68
|
+
|
|
69
|
+
## Consistency boundary
|
|
70
|
+
|
|
71
|
+
The initial correctness layer is explicit Ruby APIs and immediate component validation, not a generalized page linter.
|
|
72
|
+
|
|
73
|
+
Components fail for:
|
|
74
|
+
|
|
75
|
+
- Unknown variants, sizes, placements, alignments, types, and other closed values.
|
|
76
|
+
- Missing required content or slots.
|
|
77
|
+
- Invalid slot combinations and cardinalities the component can know.
|
|
78
|
+
- Attempts to replace reserved Nitro identity or state attributes.
|
|
79
|
+
- Direct `class:` or `style:` arguments.
|
|
80
|
+
|
|
81
|
+
This guarantees use of Nitro Kit's public vocabulary. It does not claim to prove whole-page information architecture, aesthetics, heading quality, or emphasis.
|
|
82
|
+
|
|
83
|
+
### Public attributes
|
|
84
|
+
|
|
85
|
+
Common element semantics may be first-class keywords: `id:`, `href:`, `type:`, `name:`, `value:`, `disabled:`, `required:`, and similar.
|
|
86
|
+
|
|
87
|
+
Less common native attributes use explicit boundaries:
|
|
88
|
+
|
|
89
|
+
- `html:` for ordinary attributes.
|
|
90
|
+
- `aria:` for ARIA attributes.
|
|
91
|
+
- `data:` for non-reserved application data and additive Stimulus controllers/actions.
|
|
92
|
+
|
|
93
|
+
Two lists in `NitroKit::Component` define the boundary. `COMPONENT_OWNED_DATA_ATTRIBUTES` is `state`, `disabled`, `required`, `orientation`, `presentation`, `placement`, `layout`, and `field-type` — keys a component sets for itself through its internal `attributes:` bag. `RESERVED_DATA_ATTRIBUTES` adds `nk`, `slot`, `variant`, `size`, `nk-escape`, and `enhanced`; applications may not pass any entry in that combined list through `data:`.
|
|
94
|
+
|
|
95
|
+
Application `data-controller` and `data-action` values compose with component-owned values; other owned-data collisions raise.
|
|
96
|
+
|
|
97
|
+
### Class escape hatch
|
|
98
|
+
|
|
99
|
+
Nitro components never emit or depend on classes. The only exception is:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
NitroKit::Button.new(
|
|
103
|
+
"Third-party integration",
|
|
104
|
+
desperately_need_a_class: "external-widget-trigger"
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
It emits both the requested class and `data-nk-escape="class"`. Blank and non-string values raise. Nitro-owned components, blocks, and examples do not use the escape. Legitimate application content enters through named compound methods or normal Phlex composition.
|
|
109
|
+
|
|
110
|
+
## Architecture
|
|
111
|
+
|
|
112
|
+
Nitro Kit has three ownership surfaces:
|
|
113
|
+
|
|
114
|
+
1. **Behavior** — Nitro-owned Stimulus controllers and native browser behavior.
|
|
115
|
+
2. **Structure** — Nitro-owned Phlex atoms, layouts, blocks, and application shells.
|
|
116
|
+
3. **Aesthetics** — Nitro-owned static CSS driven by public custom properties.
|
|
117
|
+
|
|
118
|
+
The surfaces are distinct but coordinated. Behavior and CSS rely on the rendered structure, while applications interact through Ruby constructors, compound methods, and theme variables.
|
|
119
|
+
|
|
120
|
+
### Self-describing markup
|
|
121
|
+
|
|
122
|
+
Every visual root has a stable identity:
|
|
123
|
+
|
|
124
|
+
```html
|
|
125
|
+
<button data-nk="button" data-variant="primary" data-size="md"></button>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Owned parts use component-qualified slots:
|
|
129
|
+
|
|
130
|
+
```html
|
|
131
|
+
<div data-nk="field">
|
|
132
|
+
<label data-slot="field-label">Email</label>
|
|
133
|
+
<input data-nk="input" data-slot="field-control" />
|
|
134
|
+
<p data-slot="field-description">Used for receipts.</p>
|
|
135
|
+
</div>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Native elements remain native. Nitro reflects application-owned state through ARIA and `data-state`; it does not mirror state already owned by a native disclosure, dialog, or popover. Slot selectors are scoped to their owner and use direct relationships where practical.
|
|
139
|
+
|
|
140
|
+
## Shipped structure vocabulary
|
|
141
|
+
|
|
142
|
+
The authoritative initializer, root, closed-option, and cardinality inventory lives in [`component_contracts.md`](component_contracts.md).
|
|
143
|
+
|
|
144
|
+
The stable release contains 40 atoms and components:
|
|
145
|
+
|
|
146
|
+
- Actions, display, and navigation: Alert, AppNavigation, Avatar, AvatarStack, Badge, Button, ButtonTo, ButtonGroup, CommandPalette, Icon, Pagination.
|
|
147
|
+
- Forms: AppearancePicker, Checkbox, CheckboxGroup, ControlGroup, Dropzone, Field, FieldGroup, Fieldset, Input, Label, RadioButton, RadioButtonGroup, RichTextArea, Select, Switch, Textarea.
|
|
148
|
+
- Structured content and interaction: Accordion, Card, Combobox, DetailsTable, Dialog, Dropdown, ProgressiveImage, Sheet, Table, Tabs, Toast, Tooltip, Typeset.
|
|
149
|
+
|
|
150
|
+
The non-visual `AppearanceBootstrap` installs the shared document appearance runtime, and `NitroKit::Choice` is the typed option value shared by the choice controls.
|
|
151
|
+
|
|
152
|
+
There is no Datepicker component and no date controller. `Input`'s `type: :date` is the only date control; `Field` and `FormBuilder` reach it through `as: :date`, and their CSS inherits the Safari date-editor alignment fix. `Combobox::Option` is likewise gone in favor of `Choice`.
|
|
153
|
+
|
|
154
|
+
### Native interaction authority
|
|
155
|
+
|
|
156
|
+
Nitro uses current evergreen HTML primitives as the source of truth before adding JavaScript:
|
|
157
|
+
|
|
158
|
+
- Accordion items are native `details`/`summary` disclosures. Single mode uses one shared `name`; it has no controller or disabled-item abstraction.
|
|
159
|
+
- Dialog declarations produce exactly one native panel through the required `panel(title:, description: nil, nonmodal: false)` declaration, and Nitro renders close button, title, description, then application content inside it. `command="show-modal"` and `command="close"` controls target the panel through `commandfor`; `nonmodal: true` is the only server-rendered open mode and cannot be combined with a trigger. `nk--dialog` adds only backdrop light dismissal and, for `dismissible: false`, Escape suppression.
|
|
160
|
+
- Dropdown visibility and invoker state belong to `popover="auto"`. `trigger` forwards `icon:`, `icon_end:`, and `label:` to Button, and `item` accepts its own `icon:`. Its small controller supplies menu focus, arrow/Home/End navigation, and focus restoration to the trigger when the popover closes with focus still inside it. CSS anchor positioning follows the trigger when supported and otherwise centers the menu safely in the viewport.
|
|
161
|
+
- CommandPalette uses one native dialog, a declarative search-shaped trigger, and native destination links. Its controller adds the optional Command-K/Control-K shortcut, local filtering, result announcements, and Turbo cleanup without replacing link navigation or retaining hidden application policy. With `search_url:`, the same input submits debounced GET requests into the owned Turbo Frame; the endpoint returns `CommandPalette::Results` HTML and remains responsible for authorization.
|
|
162
|
+
- Tooltip visibility belongs to CSS hover and focus selectors, including a hoverable bridge across the visual gap. Button triggers cover ordinary buttons and links; `as: :custom` forwards owned HTML, ARIA, and data to an existing focusable mutation or compound trigger. Its controller only implements Escape dismissal and reset.
|
|
163
|
+
|
|
164
|
+
These components do not synchronize browser state into redundant `data-state` or explicit ARIA attributes. JavaScript fills semantic interaction gaps without replacing native ownership.
|
|
165
|
+
|
|
166
|
+
### Variant axes
|
|
167
|
+
|
|
168
|
+
`variant:` and `size:` on a component root are its identity axes, emitted as `data-variant` and `data-size` by the base component. A slot may carry its own owned `data-variant` when the slot has variant identity of its own: `Toast::Item` is a nested component with its own root variant, and Dropdown `item` is a plain element that takes `slot_attributes(:item, variant:)`, the base component's owned slot-variant channel. Caller `data: { variant: }` stays reserved in both cases.
|
|
169
|
+
|
|
170
|
+
Each component has exactly one variant axis. `Alert` variants are `default info success warning error`, the same vocabulary as `Toast::Item`, and the tint comes from the Alert-owned `VARIANT_PALETTE`; there is no `color:` option on Alert. `Badge` keeps a separate `color:` palette axis alongside its `default outline` variants, and that palette is Badge's alone.
|
|
171
|
+
|
|
172
|
+
`Button` icons are `icon:` and `icon_end:`, matching the `button-icon` and `button-icon-end` slots. There is no `icon_right:`.
|
|
173
|
+
|
|
174
|
+
Three layout primitives are public:
|
|
175
|
+
|
|
176
|
+
- `Flex(dir:, gap: 4, align: :start, justify: :start, wrap: :nowrap)`
|
|
177
|
+
- `Grid(cols:, gap: 4)`
|
|
178
|
+
- `Container(size:)`
|
|
179
|
+
|
|
180
|
+
`Flex` replaces the former `VStack` and `HStack` components. `dir:` is required and accepts `row`, `col`, `row-reverse`, or `col-reverse`. Alignment accepts `start`, `center`, `end`, `stretch`, or `baseline`; justification accepts `start`, `center`, `end`, `between`, `around`, or `evenly`; wrapping accepts `nowrap`, `wrap`, or `wrap-reverse`. Responsive strings and rendered attributes use those hyphenated tokens; idiomatic Ruby scalar symbols use `:row_reverse`, `:col_reverse`, and `:wrap_reverse` and normalize to them. `Grid` accepts `1..12` columns. Both layouts accept gaps `0`, `1`, `2`, `3`, `4`, `5`, `6`, `8`, `10`, `12`, and `16`.
|
|
181
|
+
|
|
182
|
+
Every Flex and Grid property accepts a scalar or a property-scoped responsive string:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
render NitroKit::Flex.new(
|
|
186
|
+
dir: "col md:row",
|
|
187
|
+
gap: "3 md:6",
|
|
188
|
+
align: "stretch md:center",
|
|
189
|
+
justify: "start md:between"
|
|
190
|
+
) do
|
|
191
|
+
render WorkspaceSummary.new
|
|
192
|
+
render WorkspaceActions.new
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
render NitroKit::Grid.new(cols: "1 sm:2 lg:3", gap: "3 lg:6") do
|
|
196
|
+
records.each { |record| render RecordCard.new(record) }
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Each string follows `BASE sm:VALUE md:VALUE lg:VALUE xl:VALUE 2xl:VALUE`. The unprefixed base is required and applies mobile-first. The fixed minimum-width prefixes are `sm` 40rem, `md` 48rem, `lg` 64rem, `xl` 80rem, and `2xl` 96rem. Inputs normalize to base then breakpoint order; the corresponding `data-dir`, `data-gap`, `data-align`, `data-justify`, `data-wrap`, or `data-cols` attribute preserves that normalized string. Duplicate or unknown prefixes, missing base values, and values outside the property's closed vocabulary raise `ArgumentError`.
|
|
201
|
+
|
|
202
|
+
This is a small typed layout API, not a general utility language. Nitro does not parse Tailwind classes, require Tailwind at runtime, accept arbitrary or customizable breakpoints, support max/range/container prefixes, or expose arbitrary CSS values.
|
|
203
|
+
|
|
204
|
+
Eleven blocks and shells were extracted:
|
|
205
|
+
|
|
206
|
+
- `AuthShell`
|
|
207
|
+
- `AppShell`
|
|
208
|
+
- `SettingsLayout`
|
|
209
|
+
- `Toolbar`
|
|
210
|
+
- `PaginationBar`
|
|
211
|
+
- `PageHeader`
|
|
212
|
+
- `StatGrid`
|
|
213
|
+
- `DataSection`
|
|
214
|
+
- `FormSection`
|
|
215
|
+
- `DangerZone`
|
|
216
|
+
- `EmptyState`
|
|
217
|
+
|
|
218
|
+
These are intentionally a small vocabulary rather than a general page framework. Product state, routes, authorization, domain policy, and copy remain application-owned.
|
|
219
|
+
|
|
220
|
+
## Shipped 2.0 additions
|
|
221
|
+
|
|
222
|
+
The following additions ship in 2.0. Their exact public constructors and compound cardinalities are recorded in [`component_contracts.md`](component_contracts.md). Existing Nitro ownership, validation, classless markup, CSS, and testing rules apply without exception.
|
|
223
|
+
|
|
224
|
+
### Appearance
|
|
225
|
+
|
|
226
|
+
Nitro Kit owns the complete light, dark, and system appearance lifecycle. Applications render the bootstrap in `head` before stylesheet links, then place the appearance control wherever appearance is selected:
|
|
227
|
+
|
|
228
|
+
```ruby
|
|
229
|
+
render NitroKit::AppearanceBootstrap.new(
|
|
230
|
+
default: :system,
|
|
231
|
+
nonce: content_security_policy_nonce
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
render NitroKit::AppearancePicker.new(
|
|
235
|
+
id: "workspace-appearance",
|
|
236
|
+
label: "Appearance"
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`AppearanceBootstrap` is a non-visual, gem-owned script component. Its `default:` accepts only `:light`, `:dark`, or `:system`; `nonce:` supports the host's Rails content-security policy. Its JavaScript body is fixed and hashable; `default` is read from a data attribute rather than interpolated into the body. The documented stable hash and the optional nonce therefore support the two ordinary Rails CSP strategies.
|
|
241
|
+
|
|
242
|
+
The bootstrap installs one idempotent document runtime before CSS-visible paint. That runtime reads and validates `nitro-kit-appearance`, writes `data-theme-preference` plus effective `data-theme="light|dark"`, owns the single `matchMedia` listener, listens for cross-tab storage changes, and broadcasts one appearance-change event. It exists when a page has zero pickers and is not duplicated when a page has several. Storage denial or malformed data falls back to `default:` without throwing or blocking the page.
|
|
243
|
+
|
|
244
|
+
`AppearancePicker` reads the initialized document preference, and `preference:` renders a server-persisted choice as the initial `data-state`, checked radio, selected option, and trigger icon so a stored preference does not flash. Its root varies with `presentation:`: segmented and radio presentations render `fieldset[data-nk="appearance-picker"]`, select renders `label[data-nk="appearance-picker"]` wrapping a native `select`, and dropdown renders `div[data-nk="appearance-picker"]` around an icon-only sun, moon, or monitor trigger with icon-led Light, Dark, and System menu buttons. Its `nk--appearance` controller only requests preference changes and synchronizes controls from runtime events. Any number of pickers remain in sync. Native inputs remain labelled and operable without custom pointer behavior, the dropdown composes Nitro's native Popover menu, and each picker releases its subscription on disconnect.
|
|
245
|
+
|
|
246
|
+
CSS supplies a system-color fallback if the bootstrap cannot run. Appearance initialization must not flash a persisted explicit choice or leave an incorrect selection. Tests cover zero, one, and multiple pickers; system and cross-tab changes; repeated bootstrap execution; Turbo removal and reconnection; denied and malformed storage; and both documented CSP modes. Nitro does not synchronize preferences to a user record; applications may do that separately.
|
|
247
|
+
|
|
248
|
+
### Application shell and navigation
|
|
249
|
+
|
|
250
|
+
`AppShell` absorbs the former Pro Sidebar and Top Navigation concepts without copying an application layout:
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
render NitroKit::AppShell.new(id: "workspace", layout: :sidebar) do |shell|
|
|
254
|
+
shell.brand { render ProductMark.new }
|
|
255
|
+
shell.navigation do
|
|
256
|
+
render NitroKit::AppNavigation.new(label: "Primary") do |navigation|
|
|
257
|
+
navigation.body do
|
|
258
|
+
navigation.section do
|
|
259
|
+
navigation.item("Dashboard", href: dashboard_path, current: true)
|
|
260
|
+
navigation.item("Projects", href: projects_path)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
shell.topbar { render AccountActions.new }
|
|
266
|
+
shell.main { render DashboardPage.new }
|
|
267
|
+
end
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Every `AppShell` layout requires exactly one `navigation` and one `main`; `brand` and `topbar` are optional and unique. `:sidebar` places brand and navigation in a sticky, independently scrolling desktop sidebar and uses a compact mobile topbar for brand, disclosure, and optional topbar actions. `:topbar` places brand, the same navigation tree, and optional actions in a sticky desktop header, then reflows that navigation into the narrow drawer. `:hybrid` uses the sidebar placement plus a sticky desktop topbar. Product routes, authorization, current-destination policy, page titles, and copy remain application-owned.
|
|
271
|
+
|
|
272
|
+
The root is `div[data-nk="app-shell"][data-layout]`; `layout:` is the shell's structural axis and the shell has no `variant:`. Owned regions use the qualified slots `app-shell-skip-link`, `app-shell-header`, `app-shell-brand`, `app-shell-mobile-trigger`, `app-shell-topbar`, `app-shell-sidebar`, `app-shell-navigation`, `app-shell-dialog`, `app-shell-mobile-close`, and `app-shell-main`. The semantic header groups brand, mobile disclosure, and topbar actions. The narrow drawer is a real `dialog[data-slot="app-shell-dialog"]` rendered beside the sidebar, so the browser owns modal semantics; the desktop navigation wrapper stays neutral because its nested `AppNavigation` is already the landmark. One navigation DOM tree moves between the two, never cloned.
|
|
273
|
+
|
|
274
|
+
`AppNavigation` renders `nav[data-nk="app-navigation"][aria-label]` with optional unique `header` and `footer` regions around one required `body`. The body is a `ul[data-slot="app-navigation-body"]` and every entry is an `li`: `section(label: nil)` renders `li > span[data-slot="app-navigation-section-label"] + ul`, while `divider` and `spacer` each render an aria-hidden `li`. `item(text, href:, icon: nil, badge: nil, badge_color: :neutral, current: false, ...)` renders `li > a[data-slot="app-navigation-item-link"]` and carries the item's own `html:`, `aria:`, `data:`, and class escape onto that link. The body requires at least one item, at most one item is current, and a current item uses native `aria-current="page"` plus `data-state="current"` on the link.
|
|
275
|
+
|
|
276
|
+
`SettingsLayout` uses the same item vocabulary at a smaller scale: exactly one `navigation(label:)` and one `content`, where the navigation requires at least one `item(text, href:, current: false)` and renders `nav > ul[data-slot="settings-layout-items"] > li > a[data-slot="settings-layout-item-link"]`. It has no sections, dividers, spacers, icons, or badges.
|
|
277
|
+
|
|
278
|
+
The shell owns a minimum full-viewport canvas and overscroll color; sticky sidebar/topbar positioning; independent navigation overflow; and an unconstrained main region in which the application may render `Container`. The `nk--app-shell` controller owns only narrow-screen disclosure. Off-canvas hiding begins only after it marks the shell `data-enhanced`, so narrow no-JavaScript pages retain visible navigation. Opening at narrow width moves the navigation subtree into the shell's `dialog` and calls `showModal()`, so focus containment, background inertness, Escape, and the top layer are the browser's. The controller reflects open state through the root's `data-state` and the trigger's `aria-expanded` and swapped `aria-label`, and it closes on the close control, dialog cancel, backdrop activation, and Turbo navigation, returning the navigation subtree to the desktop wrapper and focus to the trigger. Disconnect and desktop resize restore the no-JavaScript tree and drop `data-enhanced`. Desktop navigation never retains drawer semantics or an inapplicable accessible name. Nitro owns the responsive breakpoint; no public arbitrary-breakpoint option is added.
|
|
279
|
+
|
|
280
|
+
Shells initially add `--nk-app-shell-sidebar-width`, `--nk-app-shell-topbar-height`, `--nk-app-shell-background`, `--nk-app-shell-sidebar-background`, `--nk-app-shell-sidebar-foreground`, `--nk-app-shell-sidebar-accent`, `--nk-app-shell-sidebar-accent-foreground`, and `--nk-app-shell-border`. Other styling consumes existing spacing, shadow, motion, and content-width tokens.
|
|
281
|
+
|
|
282
|
+
### Former Pro capabilities
|
|
283
|
+
|
|
284
|
+
Former Pro source is reference material, not migration input. Accepted capabilities are rebuilt as gem-owned Phlex components:
|
|
285
|
+
|
|
286
|
+
- `DetailsTable` composes the existing `Table`. `DetailsTable.new(record, route_base: nil)` exposes `field(attribute, label: nil, value: UNSET)` and `fields(*attributes)`. A field block receives the resolved value and owns its rendered value. Explicit `nil` is distinct from an omitted value. Automatic Rails value rendering is deterministic and tested; applications can always supply content explicitly.
|
|
287
|
+
- `ProgressiveImage.new(attachment:, alt:, size: :md, decorative: false)` accepts only `sm`, `md`, and `lg` sizes. It renders `div[data-nk="progressive-image"]` with qualified placeholder, image, and fallback slots and states `empty`, `loading`, `loaded`, and `error`. Non-decorative attached images require useful alt text. Its controller owns image decoding and load/error reflection, removes listeners on disconnect, and mutates state rather than classes.
|
|
288
|
+
- `Dropzone.new(id:, name:, label:, description: nil, direct_upload: true, multiple: false, accept: nil, max_files: 1, max_bytes: nil, disabled: false, required: false)` renders a native file input inside `div[data-nk="dropzone"]`. `label:` is the visible prompt and defaults from the `nitro_kit.dropzone` locale scope; it replaced the former `title:` keyword. It exposes idle, drag, uploading, success, error, and disabled states; qualified input, message, preview-list, progress, error, and remove-control slots; and ordinary form submission when JavaScript is unavailable. The Nitro controller integrates Active Storage direct uploads, cancellation, removal, form submission state, and Turbo teardown. Nitro does not ship or require Dropzone.js and does not expose a raw JavaScript options hash.
|
|
289
|
+
- Sorting is part of `Table` itself; there is no separate `SortableTable` component. `Table.new(sort: nil, direction: nil)` exposes sortable headers through `th(text = nil, align: :left, scope: :col, sort: nil, href: nil, sort_data: {})`. `sort:` and `direction:` are both set or both nil, directions are `asc` or `desc`, sort keys must be unique, `href:` requires `sort:`, and an omitted header text falls back to the humanized sort key. The application supplies URLs and owns sort policy. The active header renders native `aria-sort` and a direction icon; sortable but inactive headers render `aria-sort="none"` and a neutral icon. The shipped gallery recipe adapts `Ransack::Search` to this API while keeping filter allowlists and pagination policy in application code; Ransack is a development/test dependency only, not a Nitro runtime dependency, and `NitroKit::RansackTable` is not a core contract.
|
|
290
|
+
|
|
291
|
+
`DetailsTable` and sortable `Table` headers require no Nitro JavaScript. `ProgressiveImage` requires Active Storage variants and the host application's configured image processor. `Dropzone` requires Active Storage direct-upload support. These integrations do not add mandatory Ransack, Dropzone.js, or image-processing gems to Nitro Kit itself.
|
|
292
|
+
|
|
293
|
+
The audited former-Pro catalog maps completely to the new architecture:
|
|
294
|
+
|
|
295
|
+
| Former surface | 2.0 disposition |
|
|
296
|
+
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
297
|
+
| Sidebar and Top Navigation layouts | Rebuilt as `AppShell` for whole-application navigation and `Sheet` for contextual narrow panels |
|
|
298
|
+
| Details Table | Rebuilt as `DetailsTable` |
|
|
299
|
+
| Dropzone | Rebuilt as native `Dropzone`; Dropzone.js is removed |
|
|
300
|
+
| Progressive Image | Rebuilt as `ProgressiveImage` |
|
|
301
|
+
| Ransack Table | Generic sortable `Table` plus an optional gallery Ransack recipe |
|
|
302
|
+
| Currency Field | Deliberately omitted: it was an unpublished stub with an unused `cents` option and no settled locale, precision, or submitted-value semantics |
|
|
303
|
+
| Download helpers, installers, and whole-layout templates | Removed; they contradict gem ownership and direct Phlex composition |
|
|
304
|
+
|
|
305
|
+
### Customization
|
|
306
|
+
|
|
307
|
+
The customization contract remains documented public custom properties. Applications override public `--nk-*` variables in their own CSS, scoped globally or beneath an application-owned theme root. They do not replace Nitro markup, edit generated distribution CSS, or depend on private `--_nk-*` variables.
|
|
308
|
+
|
|
309
|
+
The interactive theme customizer is documentation-site software, not gallery or gem software. The gallery proves the theming contract itself: the documented token set in `docs/customization.md`, the set declared in `src/stylesheets/nitro_kit/tokens.css`, and the set served by the bundled stylesheet are asserted to be the same set; component CSS is asserted to consume only declared public tokens; and scoped `--nk-*` overrides on an application-owned wrapper are proven to reach Nitro descendants through inheritance.
|
|
310
|
+
|
|
311
|
+
### Addition verification contract
|
|
312
|
+
|
|
313
|
+
Every addition receives direct Ruby contract tests, invalid-vocabulary tests, accessibility assertions, deterministic CSS coverage, and catalog-driven browser coverage. Interactive components exercise keyboard input, narrow and wide layouts, reduced motion, Turbo Drive/Frame/Stream/morph lifecycles, disconnect cleanup, and repeated connection. Appearance coverage begins at a cold document load and proves pre-paint restoration, nonce and hash CSP rendering, denied storage, malformed storage, persistence, zero and multiple pickers, cross-tab updates, and live system changes.
|
|
314
|
+
|
|
315
|
+
`Dropzone` associates its native input, description, errors, and live status; exposes native progress semantics; supports keyboard selection and `direct_upload: false`; and keeps ordinary form submission usable without JavaScript. `ProgressiveImage` exposes exactly one accessible image while its placeholder is decorative; its fallback communicates an actual empty or error state without duplicating alt text. `AppShell` supplies a skip link and one identifiable main landmark, labels navigation, gives the narrow drawer modal/inert semantics, and preserves a logical focus order.
|
|
316
|
+
|
|
317
|
+
The gallery includes, at minimum, sidebar, topbar, and hybrid application compositions; appearance persistence and simulated system changes; every former Pro state; long and missing content; upload success, error, cancellation, and removal; progressive-image empty, loading, loaded, and failed states; and sortable-table gallery empty and populated results. Every example uses `Gallery::Example` Preview and Code tabs whose source is extracted from the executable Phlex block or concrete composition method. Combination pages deliberately mix shells, forms, tables, uploads, images, overlays, and all three appearances.
|
|
318
|
+
|
|
319
|
+
Public documentation includes `docs/customization.md` plus aligned README and Rails-integration sections. It catalogs supported tokens, stylesheet load order, global and scoped overrides, light/dark selectors, appearance bootstrap and picker setup, CSP nonce/hash configuration, shell composition, and complete copyable Rails examples.
|
|
320
|
+
|
|
321
|
+
`docs/agent_guide.md` is the packaged routing layer for coding agents. It points to version-matched Rails conventions, component contracts, Hotwire guidance, and recipes for queryable collections, resource forms, destructive actions, flash/toast feedback, and inline editing. The packaged plugin contains consumer Rails, UI, and Hotwire skills that resolve the installed gem before reading these docs. The setup generator installs the same thin routers into supported project-local skill directories and maintains a bounded `AGENTS.md` block. It does not duplicate the component registry or add an MCP server.
|
|
322
|
+
|
|
323
|
+
## CSS and themes
|
|
324
|
+
|
|
325
|
+
Nitro Kit ships `nitro_kit.css` as browser-ready plain CSS with no Tailwind requirement.
|
|
326
|
+
|
|
327
|
+
Internal cascade order is deterministic:
|
|
328
|
+
|
|
329
|
+
```text
|
|
330
|
+
nitro-kit.tokens
|
|
331
|
+
nitro-kit.reset
|
|
332
|
+
nitro-kit.base
|
|
333
|
+
nitro-kit.variant
|
|
334
|
+
nitro-kit.size
|
|
335
|
+
nitro-kit.state
|
|
336
|
+
nitro-kit.compound
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Selectors use `:where()` for zero authored specificity. Nitro ships its own global, Preflight-style reset in the `nitro-kit.reset` layer and never depends on Tailwind Preflight. It normalizes the whole page, so arbitrary content inside a component gets the same baseline as Nitro's own markup, and because it is layered, unlayered application CSS always wins. `min-width: 0` and list-marker removal stay scoped to Nitro-owned elements so prose lists keep real markers.
|
|
340
|
+
|
|
341
|
+
Public `--nk-*` variables cover semantic colors, the component-specific raised default-button treatment, typography, spacing, radii, border and focus geometry, shadows, motion, control heights, and content widths. Private `--_nk-*` variables coordinate component mechanics and are not theme API.
|
|
342
|
+
|
|
343
|
+
The exact browser order is optional third-party base CSS such as Lexxy, optional `nitro_kit-tailwind-v4.css`, generated `nitro_kit.css`, compiled Tailwind CSS when present, then unlayered application styles containing token overrides. `AppearanceBootstrap` precedes every stylesheet. Applications never edit the generated asset. [`customization.md`](customization.md) is the complete public token inventory and usage guide.
|
|
344
|
+
|
|
345
|
+
Before JavaScript connects, `:root` follows `prefers-color-scheme`; explicit `[data-theme="light"]` and `[data-theme="dark"]` contracts override it and set the matching `color-scheme`. `nk--appearance` keeps `data-theme` equal to the effective light or dark appearance and records the selected light, dark, or system preference separately. Applications override the same public variables for both theme contracts.
|
|
346
|
+
|
|
347
|
+
`nitro_kit-tailwind-v4.css` is an optional, separately versioned adapter. It establishes layer order and maps theme variables. Tailwind compilation, source detection, and utilities remain application concerns, not Nitro runtime dependencies.
|
|
348
|
+
|
|
349
|
+
## Layout sizing
|
|
350
|
+
|
|
351
|
+
Parents own external placement and available width. Components own intrinsic geometry.
|
|
352
|
+
|
|
353
|
+
Inputs, textareas, selects, tables, and broad surfaces may stretch naturally. Buttons, badges, avatars, icons, and switches remain intrinsic unless an accepted layout explicitly stretches them.
|
|
354
|
+
|
|
355
|
+
## Rails and Hotwire
|
|
356
|
+
|
|
357
|
+
`NitroKit::FormBuilder` is selected through Rails `form_with` from Phlex. It preserves Rails naming, IDs, model values, values-before-type-cast, Active Model errors, multipart forms, checkbox hidden values, and captured select options.
|
|
358
|
+
|
|
359
|
+
Turbo Frames and Turbo Streams remain Rails helpers used directly from Phlex. Invalid submissions return 422; successful non-Turbo form submissions redirect with 303. The supported boundary and executable reference path are documented in [`rails_integration.md`](rails_integration.md).
|
|
360
|
+
|
|
361
|
+
Server-rendered feedback is the Rails flash. `Toast` renders `section[data-nk="toast"][role="region"]` wrapping `ol[data-slot="toast-list"]`, whose id is the toast id plus `-list`, so the default region is addressable as `nk-toast-list` and a Turbo Stream can append `NitroKit::Toast::Item` to it directly. Items carry `role="status"`, or `role="alert"` for the error variant, so a server-rendered item announces without waiting for a DOM mutation, and every item is `data-turbo-temporary` so a cached page never replays stale feedback while the region and list survive. `Toast::FlashMessages` maps an enumerable Rails flash onto the same items. Nitro does not add a client-side notification store.
|
|
362
|
+
|
|
363
|
+
The engine ships CSS assets and Nitro-owned Stimulus controllers for enhancements that native HTML and CSS do not cover. When importmap is present it adds its pins automatically; the host still owns Stimulus and its normal controller loader. The engine boots without importmap: Accordion is complete with no controller at all and Dialog still opens and closes through declarative `command`/`commandfor`, while enhanced interactions such as Dropdown keyboard navigation, Tooltip Escape dismissal, and Dialog backdrop light dismissal require their pinned controllers. Nitro Kit 2.0 does not define a JavaScript-package entrypoint for automatic bundler registration.
|
|
364
|
+
|
|
365
|
+
Date inputs, Switch, and ordinary checked state deliberately use native inputs rather than custom controllers. The one exception is `indeterminate:`, which HTML cannot express as an attribute: `Checkbox` mounts `nk--checkable` only in that case, and the controller's whole job is to apply the native DOM property and own the matching `data-state="indeterminate"`. No third-party JavaScript runtime is vendored.
|
|
366
|
+
|
|
367
|
+
## Examples and verification
|
|
368
|
+
|
|
369
|
+
The dummy Rails application is the canonical example gallery. An explicit `Gallery::Catalog` drives routes for component pages and realistic application compositions. Components carry a subcategory (layout, navigation, forms, data, feedback, actions) that groups the sidebar; compositions are the executable whole-system tests. That catalog is documentation infrastructure, not a packaged public registry.
|
|
370
|
+
|
|
371
|
+
Examples cover closed options, content pressure, native state, validation, empty/error/loading/destructive states, light and dark themes, narrow and wide layouts, and interaction between forms, navigation, tables, and overlays.
|
|
372
|
+
|
|
373
|
+
Examples also cover system appearance, complete application shells, former Pro capabilities, and dense cross-component combinations. Previewed Ruby and copied Ruby remain the same executable source.
|
|
374
|
+
|
|
375
|
+
The gallery's three top-level pages are audience-oriented. Introduction states what the gallery is and who each surface serves. The agent guide at `/gallery/agent-guide` is the machine entry point: the composition model, how every component page is self-contained, why the system refuses `class:` and catch-all options, and the same `Gallery::AgentRules` rules every component page renders. The human guide at `/gallery/guide` explains how to read a component page, what the compositions are, and theming basics, and points to nitrokit.dev for guides, the theme customizer, and Pro. `/llms.txt` serves the agent guide as `text/plain`, rendered by `Gallery::LlmsText` from `Gallery::AgentGuide`, `Gallery::AgentRules`, and `Gallery::Catalog` — never a second copy of the text.
|
|
376
|
+
|
|
377
|
+
Every component page is self-contained for an agent that fetches only that page. After the examples it renders three reference sections outside every example canvas: the component's own row from `docs/component_contracts.md`, inline summaries of the `docs/patterns/*.md` conventions the catalog maps to that page, and the shared system rules. Each section is source-referenced and render-inlined — one copy in the source, one copy on every page. `Gallery::AgentRules` owns the rules text and reads `NitroKit::Component::RESERVED_DATA_ATTRIBUTES` and its neighbours at render time, `Gallery::Contracts` parses the shipped contract table, `Gallery::Patterns` reads each pattern document's leading `## Summary` section, and `Gallery::Catalog::PATTERNS` declares which patterns a page carries.
|
|
378
|
+
|
|
379
|
+
Primary verification remains:
|
|
380
|
+
|
|
381
|
+
- Direct-Phlex render and contract tests.
|
|
382
|
+
- Invalid vocabulary and reserved-attribute tests.
|
|
383
|
+
- Rails request/integration tests.
|
|
384
|
+
- Deterministic CSS build and package audits.
|
|
385
|
+
- Catalog-driven browser behavior and Turbo-lifecycle verification.
|
|
386
|
+
|
|
387
|
+
## Explicitly outside 2.0
|
|
388
|
+
|
|
389
|
+
The following are not current contracts:
|
|
390
|
+
|
|
391
|
+
- Generated component copies or compatibility helpers.
|
|
392
|
+
- A public registry, JSON Schema, document linter, MCP server, or custom-element runtime.
|
|
393
|
+
- `Spacer`, `Split`, or `Frame` layout primitives.
|
|
394
|
+
- `MarketingShell` or `AuthenticationPanel` shells.
|
|
395
|
+
- `ProgressSteps` or speculative domain blocks.
|
|
396
|
+
- Card/Table density, radio-group orientation, arbitrary grid tracks, custom breakpoints, max/range/container queries, or a generic utility DSL.
|
|
397
|
+
- Theme marketplaces, remote preset registries, arbitrary CSS editing, or visual-regression goldens.
|
|
398
|
+
- Downloadable installers, generated helpers, copied components, or copied application layouts.
|
|
399
|
+
- `CurrencyField` in this expansion wave.
|
|
400
|
+
|
|
401
|
+
These ideas require new evidence and a deliberate future API decision. Their appearance in design notes or historical plans does not make them public.
|
|
402
|
+
|
|
403
|
+
## Design history
|
|
404
|
+
|
|
405
|
+
The pivot was delivered in evidence-seeking stages: establish the kernel and representative vertical slice, migrate atoms, build atom-only product flows, record repeated friction, extract only proven layouts and page sections, expand the gallery, remove 1.x, then consolidate behavior, packaging, and documentation.
|
|
406
|
+
|
|
407
|
+
The first evidence pass rejected more abstractions than it accepted. `Spacer`, `Split`, `Frame`, App/Marketing shells, AuthenticationPanel, and ProgressSteps stayed out because those flows did not establish a stable cross-domain responsibility. That pass initially extracted separate `VStack` and `HStack` components plus a fixed three-column Grid. The later responsive-layout consolidation superseded those APIs with unified `Flex` and responsive `Grid`. The application-layout mandate and the Sidebar and Top Navigation reference audit supplied enough repeated responsibility to approve `AppShell`; MarketingShell, AuthenticationPanel, and speculative layout primitives remain rejected.
|
|
408
|
+
|
|
409
|
+
## Definition of done
|
|
410
|
+
|
|
411
|
+
The stable 2.0 architecture requires:
|
|
412
|
+
|
|
413
|
+
- Core components are gem-owned and composed directly through Phlex.
|
|
414
|
+
- Generated copies, ERB component helpers, Tailwind Merge, and consumer Tailwind dependence are absent.
|
|
415
|
+
- Every visual component emits classless, self-describing markup and uses static Nitro CSS.
|
|
416
|
+
- Rails forms and Hotwire work through direct Phlex composition.
|
|
417
|
+
- The accepted layout and block vocabulary covers the canonical flows without escape markers.
|
|
418
|
+
- Light, dark, and system appearance remain correct across reloads, Turbo navigation, and system changes.
|
|
419
|
+
- AppShell and accepted former Pro capabilities satisfy their Ruby, DOM, accessibility, dependency, and teardown contracts.
|
|
420
|
+
- The documented public token set is the only theming contract, and it is verified end to end.
|
|
421
|
+
- CSS, package, Ruby, Rails, and browser verification pass.
|
|
422
|
+
- Public documentation matches shipped APIs and clearly states the 1.x break.
|