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,595 @@
|
|
|
1
|
+
# Customizing Nitro Kit
|
|
2
|
+
|
|
3
|
+
Nitro Kit owns component Ruby, markup, behavior, and default CSS. Applications customize the system by overriding the public `--nk-*` custom properties, composing components into application UI, and occasionally creating a narrow subclass. Applications do not copy or edit Nitro components.
|
|
4
|
+
|
|
5
|
+
## Stylesheet order
|
|
6
|
+
|
|
7
|
+
Load browser styles in this order:
|
|
8
|
+
|
|
9
|
+
1. Optional third-party base styles, such as Lexxy.
|
|
10
|
+
2. The optional `nitro_kit-tailwind-v4` adapter.
|
|
11
|
+
3. The generated `nitro_kit` distribution stylesheet.
|
|
12
|
+
4. The application's compiled Tailwind CSS, when present.
|
|
13
|
+
5. Application styles, including Nitro token overrides.
|
|
14
|
+
|
|
15
|
+
`NitroKit::AppearanceBootstrap` precedes every entry in this list. The install
|
|
16
|
+
generator owns this ordering and can safely be rerun. For example, an
|
|
17
|
+
application with Lexxy and no Tailwind has exactly three stylesheet entries:
|
|
18
|
+
|
|
19
|
+
```erb
|
|
20
|
+
<%= stylesheet_link_tag "lexxy", "nitro_kit", "application", "data-turbo-track": "reload" %>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
A Rails application without Tailwind can use:
|
|
24
|
+
|
|
25
|
+
```erb
|
|
26
|
+
<%= stylesheet_link_tag "nitro_kit", "application", "data-turbo-track": "reload" %>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
A Tailwind CSS v4 application can use:
|
|
30
|
+
|
|
31
|
+
```erb
|
|
32
|
+
<%= stylesheet_link_tag \
|
|
33
|
+
"nitro_kit-tailwind-v4", \
|
|
34
|
+
"nitro_kit", \
|
|
35
|
+
"tailwind", \
|
|
36
|
+
"application", \
|
|
37
|
+
"data-turbo-track": "reload" %>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Keep overrides unlayered in application CSS and load them after Nitro Kit. Nitro's selectors use `:where()` inside named cascade layers, so an ordinary application rule can override a token without selector escalation or `!important`.
|
|
41
|
+
|
|
42
|
+
Do not edit `app/assets/stylesheets/nitro_kit.css` in the gem or a bundled copy of it. That file is generated from `src/stylesheets/nitro_kit/` and is replaced on upgrade. Variables named `--_nk-*` are private component mechanics and may change without notice. Only the `--nk-*` variables listed below are the customization contract.
|
|
43
|
+
|
|
44
|
+
## Global overrides
|
|
45
|
+
|
|
46
|
+
Shared tokens can be changed once on the document root:
|
|
47
|
+
|
|
48
|
+
```css
|
|
49
|
+
:root {
|
|
50
|
+
--nk-font-sans: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
51
|
+
--nk-content-lg: 52rem;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Color tokens need a light value, a no-JavaScript system fallback, and a dark value. Keep the selectors in this order:
|
|
56
|
+
|
|
57
|
+
```css
|
|
58
|
+
:root,
|
|
59
|
+
[data-theme="light"] {
|
|
60
|
+
--nk-color-primary: oklch(0.55 0.2 260);
|
|
61
|
+
--nk-color-primary-foreground: oklch(0.985 0 0);
|
|
62
|
+
--nk-color-focus: oklch(0.55 0.2 260);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@media (prefers-color-scheme: dark) {
|
|
66
|
+
:root:not([data-theme]) {
|
|
67
|
+
--nk-color-primary: oklch(0.72 0.16 260);
|
|
68
|
+
--nk-color-primary-foreground: oklch(0.15 0.02 260);
|
|
69
|
+
--nk-color-focus: oklch(0.72 0.16 260);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
[data-theme="dark"] {
|
|
74
|
+
--nk-color-primary: oklch(0.72 0.16 260);
|
|
75
|
+
--nk-color-primary-foreground: oklch(0.15 0.02 260);
|
|
76
|
+
--nk-color-focus: oklch(0.72 0.16 260);
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The media query matters when JavaScript or the appearance bootstrap is unavailable. When the runtime is active, `data-theme` is always the resolved `light` or `dark` appearance. `system` is a stored preference in `data-theme-preference`, not a third palette and never a `data-theme="system"` selector.
|
|
81
|
+
|
|
82
|
+
Raised default Buttons have their own tokens, so their dark treatment can change without recoloring cards, dialogs, menus, or data-entry controls:
|
|
83
|
+
|
|
84
|
+
```css
|
|
85
|
+
@media (prefers-color-scheme: dark) {
|
|
86
|
+
:root:not([data-theme]) {
|
|
87
|
+
--nk-button-default-background: oklch(0.3 0.01 286);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
[data-theme="dark"] {
|
|
92
|
+
--nk-button-default-background: oklch(0.3 0.01 286);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Scoped overrides
|
|
97
|
+
|
|
98
|
+
Wrap a product area in an application-owned attribute when only that subtree should change:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
class BillingArea < Phlex::HTML
|
|
102
|
+
def view_template
|
|
103
|
+
div(data: { app_theme: "billing" }) do
|
|
104
|
+
render Billing::Overview.new
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Then scope shared and appearance-specific values. This example follows the document appearance set by `AppearanceBootstrap` while retaining the no-JavaScript system fallback.
|
|
111
|
+
|
|
112
|
+
```css
|
|
113
|
+
[data-app-theme="billing"] {
|
|
114
|
+
--nk-font-sans: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
115
|
+
--nk-content-md: 36rem;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
:root [data-app-theme="billing"],
|
|
119
|
+
[data-theme="light"] [data-app-theme="billing"] {
|
|
120
|
+
--nk-color-primary: oklch(0.49 0.17 155);
|
|
121
|
+
--nk-color-primary-foreground: white;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@media (prefers-color-scheme: dark) {
|
|
125
|
+
:root:not([data-theme]) [data-app-theme="billing"] {
|
|
126
|
+
--nk-color-primary: oklch(0.72 0.16 155);
|
|
127
|
+
--nk-color-primary-foreground: oklch(0.15 0.02 155);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
[data-theme="dark"] [data-app-theme="billing"] {
|
|
132
|
+
--nk-color-primary: oklch(0.72 0.16 155);
|
|
133
|
+
--nk-color-primary-foreground: oklch(0.15 0.02 155);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This changes Nitro descendants through inheritance without adding classes to them or reaching into `data-slot` markup.
|
|
138
|
+
|
|
139
|
+
## Appearance setup
|
|
140
|
+
|
|
141
|
+
Render `NitroKit::AppearanceBootstrap` in the document `head` before every stylesheet link. It resolves a validated `light`, `dark`, or `system` preference before CSS-visible paint. Render zero, one, or many pickers in the body; they all use the same document runtime.
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
class ApplicationLayout < Phlex::HTML
|
|
145
|
+
include Phlex::Rails::Layout
|
|
146
|
+
include Phlex::Rails::Helpers::ContentSecurityPolicyNonce
|
|
147
|
+
|
|
148
|
+
def view_template
|
|
149
|
+
doctype
|
|
150
|
+
|
|
151
|
+
html(lang: "en") do
|
|
152
|
+
head do
|
|
153
|
+
render NitroKit::AppearanceBootstrap.new(
|
|
154
|
+
default: :system,
|
|
155
|
+
nonce: content_security_policy_nonce
|
|
156
|
+
)
|
|
157
|
+
stylesheet_link_tag("nitro_kit", data: { turbo_track: "reload" })
|
|
158
|
+
stylesheet_link_tag("application", data: { turbo_track: "reload" })
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
body do
|
|
162
|
+
render NitroKit::AppearancePicker.new(
|
|
163
|
+
id: "application-appearance",
|
|
164
|
+
label: "Appearance"
|
|
165
|
+
)
|
|
166
|
+
yield
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The runtime persists the preference in `localStorage` under `nitro-kit-appearance`, follows live operating-system changes in system mode, and synchronizes other tabs. Storage failure leaves in-document selection working and falls back to `default:`. Nitro Kit does not synchronize the preference to an application user record.
|
|
174
|
+
|
|
175
|
+
For a nonce-based content security policy, pass Rails' `content_security_policy_nonce` as above and allow that nonce in `script-src`. For a hash-based policy, allow the exact fixed script body:
|
|
176
|
+
|
|
177
|
+
```text
|
|
178
|
+
script-src 'self' 'sha256-Vcime4euWSeYtHSfjYjqz/XhRyzMcLpn6Ip2LlaHleY='
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The value is also available as `NitroKit::AppearanceBootstrap::CSP_HASH`. The default preference is stored in a data attribute, so changing `default:` does not change the hash. Recheck the constant when upgrading because an intentional runtime change produces a new hash.
|
|
182
|
+
|
|
183
|
+
## Choosing a theme
|
|
184
|
+
|
|
185
|
+
Treat related tokens as a system:
|
|
186
|
+
|
|
187
|
+
- Accent changes usually set `--nk-color-primary`, `--nk-color-primary-foreground`, and `--nk-color-focus` for both appearances. The default hover value is derived automatically; set `--nk-color-primary-hover` only when the derived color is unsuitable.
|
|
188
|
+
- Neutral changes should coordinate canvas, surface, elevated, foreground, muted, border, and neutral-content pairs for both appearances.
|
|
189
|
+
- Default Button changes use the `--nk-button-default-*` tokens. They are separate from `--nk-color-surface` so a raised neutral action can change without recoloring inputs, cards, dialogs, and menus.
|
|
190
|
+
- Radius changes should move `--nk-radius-xs` through `--nk-radius-xl` together. Leave `--nk-radius-full` alone unless pills and circular controls should stop being fully rounded.
|
|
191
|
+
- Density changes should coordinate `--nk-space` with all five control-height tokens. Changing one component's internal gap is not a public theme contract.
|
|
192
|
+
- Font changes normally set `--nk-font-sans`; set `--nk-font-mono`, text sizes, line heights, or weights only when the whole type system calls for it.
|
|
193
|
+
|
|
194
|
+
Check foreground pairs and focus indicators for contrast in both appearances. The semantic names describe use, not a fixed hue: `danger` can be a project-appropriate destructive color, but it should remain recognizably destructive everywhere it appears.
|
|
195
|
+
|
|
196
|
+
## Theme customizer
|
|
197
|
+
|
|
198
|
+
The interactive theme customizer lives on the documentation site at
|
|
199
|
+
[nitrokit.dev/customize](https://nitrokit.dev/customize). Pick an accent, neutral, radius, density, font, and application shell, watch a complete workspace update, then copy deterministic CSS containing only changed public tokens plus a copyable `AppShell` composition for the selected layout.
|
|
200
|
+
|
|
201
|
+
Paste the CSS into an application-owned stylesheet such as `app/assets/stylesheets/nitro_theme.css`, then load that stylesheet after Nitro Kit and any compiled Tailwind CSS:
|
|
202
|
+
|
|
203
|
+
```erb
|
|
204
|
+
<%= stylesheet_link_tag \
|
|
205
|
+
"nitro_kit", \
|
|
206
|
+
"application", \
|
|
207
|
+
"nitro_theme", \
|
|
208
|
+
"data-turbo-track": "reload" %>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The customizer copies text to the clipboard. It does not download files, write into an application, or generate a component implementation.
|
|
212
|
+
|
|
213
|
+
## Application composition
|
|
214
|
+
|
|
215
|
+
Composition is the default extension mechanism. Put product policy, routes, copy, and domain objects in application components while Nitro owns the visual components:
|
|
216
|
+
|
|
217
|
+
Use an application-owned base beside Nitro Kit. Including `NitroKit` once makes
|
|
218
|
+
capitalized Kit methods available to descendants; the merge helper below is
|
|
219
|
+
ordinary application code and does not call Nitro private APIs:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
class ApplicationComponent < Phlex::HTML
|
|
223
|
+
include NitroKit
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def merge_attributes(defaults = {}, html: {}, data: {}, aria: {})
|
|
228
|
+
defaults = canonical_attributes(defaults, "defaults")
|
|
229
|
+
html = canonical_attributes(html, "HTML")
|
|
230
|
+
validate_html_boundaries!(html)
|
|
231
|
+
|
|
232
|
+
default_data = canonical_attributes(defaults.delete(:data) || {}, "default data", prefix: "data")
|
|
233
|
+
default_aria = canonical_attributes(defaults.delete(:aria) || {}, "default ARIA", prefix: "aria")
|
|
234
|
+
data = canonical_attributes(data, "data", prefix: "data")
|
|
235
|
+
aria = canonical_attributes(aria, "ARIA", prefix: "aria")
|
|
236
|
+
classes = merged_classes(defaults.delete(:class), html.delete(:class))
|
|
237
|
+
|
|
238
|
+
defaults.merge(html).tap do |attributes|
|
|
239
|
+
attributes[:class] = classes if classes
|
|
240
|
+
attributes[:data] = default_data.merge(data) if default_data.any? || data.any?
|
|
241
|
+
attributes[:aria] = default_aria.merge(aria) if default_aria.any? || aria.any?
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def canonical_attributes(value, name, prefix: nil)
|
|
246
|
+
raise ArgumentError, "#{name} must be a Hash" unless value.is_a?(Hash)
|
|
247
|
+
|
|
248
|
+
value.each_with_object({}) do |(key, item), normalized|
|
|
249
|
+
unless key.is_a?(String) || key.is_a?(Symbol)
|
|
250
|
+
raise ArgumentError, "#{name} attribute keys must be Strings or Symbols"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
key = key.to_s.downcase.tr("_", "-").to_sym
|
|
254
|
+
emitted_name = [ prefix, key ].compact.join("-")
|
|
255
|
+
raise ArgumentError, "Duplicate #{name} attribute #{emitted_name}" if normalized.key?(key)
|
|
256
|
+
|
|
257
|
+
normalized[key] = item
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def validate_html_boundaries!(html)
|
|
262
|
+
html.each_key do |key|
|
|
263
|
+
boundary = %w[data aria].find do |name|
|
|
264
|
+
key == name.to_sym || key.to_s.start_with?("#{name}-")
|
|
265
|
+
end
|
|
266
|
+
next unless boundary
|
|
267
|
+
|
|
268
|
+
raise ArgumentError, "Pass #{key} through #{boundary}:, not html:"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def merged_classes(*values)
|
|
273
|
+
tokens = values.compact.flat_map do |value|
|
|
274
|
+
raise ArgumentError, "class values must be Strings" unless value.is_a?(String)
|
|
275
|
+
|
|
276
|
+
value.split
|
|
277
|
+
end
|
|
278
|
+
tokens = tokens.reverse.uniq.reverse
|
|
279
|
+
tokens.join(" ") if tokens.any?
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The precedence is explicit:
|
|
285
|
+
|
|
286
|
+
1. Caller `html:` values replace same-key defaults.
|
|
287
|
+
2. Caller `data:` and `aria:` values replace same-key nested defaults.
|
|
288
|
+
3. Classes merge instead of replacing. Default tokens come first; caller
|
|
289
|
+
tokens come last; a duplicate survives once at its caller position.
|
|
290
|
+
|
|
291
|
+
Class attribute order does not override the CSS cascade; application
|
|
292
|
+
stylesheet source order still decides conflicts between class rules. The
|
|
293
|
+
helper canonicalizes keys to their lowercase, hyphenated HTML spelling before
|
|
294
|
+
merging, rejects nested or flattened `data-*`/`aria-*` attributes inside
|
|
295
|
+
`html:`, and never mutates the defaults. String, symbol, underscore, and dash
|
|
296
|
+
aliases therefore emit once; caller values win over defaults, while duplicate
|
|
297
|
+
aliases within one bag raise an error naming the emitted attribute.
|
|
298
|
+
|
|
299
|
+
A small reusable application component can then provide its own class-based
|
|
300
|
+
root while composing Nitro through the public Kit method:
|
|
301
|
+
|
|
302
|
+
```ruby
|
|
303
|
+
module RailsIntegration
|
|
304
|
+
class StatusPill < ApplicationComponent
|
|
305
|
+
STATUSES = %i[received reviewed].freeze
|
|
306
|
+
|
|
307
|
+
def initialize(status, html: {}, data: {}, aria: {})
|
|
308
|
+
@status = status.respond_to?(:to_sym) ? status.to_sym : status
|
|
309
|
+
raise ArgumentError, "Unknown status #{status.inspect}" unless STATUSES.include?(@status)
|
|
310
|
+
|
|
311
|
+
@attributes = merge_attributes(
|
|
312
|
+
{
|
|
313
|
+
class: "status-pill status-pill--quiet",
|
|
314
|
+
title: "Submission status",
|
|
315
|
+
data: { application_component: "status-pill", state: @status },
|
|
316
|
+
aria: { live: "polite" }
|
|
317
|
+
},
|
|
318
|
+
html:,
|
|
319
|
+
data:,
|
|
320
|
+
aria:
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def view_template
|
|
325
|
+
span(**attributes) do
|
|
326
|
+
Badge(status.to_s.humanize, color: :success, size: :sm)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
private
|
|
331
|
+
|
|
332
|
+
attr_reader :attributes, :status
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
For example, `html: { class: "receipt-state status-pill--quiet" }, data:
|
|
338
|
+
{ state: "reviewed" }, aria: { live: "assertive" }` renders the classes as
|
|
339
|
+
`status-pill receipt-state status-pill--quiet` and lets the caller replace the
|
|
340
|
+
default state and live mode. The dummy application's
|
|
341
|
+
`ApplicationComponent`, `RailsIntegration::StatusPill`, and focused component
|
|
342
|
+
test execute this exact reference implementation.
|
|
343
|
+
|
|
344
|
+
Product components that do not need application classes remain smaller:
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
module UI
|
|
348
|
+
class UpgradeNotice < Phlex::HTML
|
|
349
|
+
def view_template
|
|
350
|
+
render NitroKit::Alert.new(variant: :warning) do |alert|
|
|
351
|
+
alert.title("Plan limit reached")
|
|
352
|
+
alert.description("Upgrade to invite another teammate.")
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
A narrow subclass is acceptable when it fixes a small, stable application vocabulary and delegates everything to a public constructor:
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
module UI
|
|
363
|
+
class SaveButton < NitroKit::Button
|
|
364
|
+
def initialize(text = "Save", disabled: false, data: {})
|
|
365
|
+
super(text, variant: :primary, type: :submit, disabled:, data:)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Do not override Nitro rendering methods, private helpers, internal `Data` records, `data-slot` structure, or Stimulus state. Those are implementation details. If a subclass needs those surfaces, compose a new application component instead.
|
|
372
|
+
|
|
373
|
+
## Application shells
|
|
374
|
+
|
|
375
|
+
`AppShell` owns responsive application chrome. The caller owns one navigation tree, current-route policy, brand, account actions, and page content:
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
module Workspace
|
|
379
|
+
class Layout < Phlex::HTML
|
|
380
|
+
include Phlex::Rails::Helpers::Routes
|
|
381
|
+
|
|
382
|
+
def initialize(page:)
|
|
383
|
+
@page = page
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def view_template
|
|
387
|
+
render NitroKit::AppShell.new(id: "workspace", layout: :sidebar) do |shell|
|
|
388
|
+
shell.brand { strong { "Northstar" } }
|
|
389
|
+
|
|
390
|
+
shell.navigation do
|
|
391
|
+
render NitroKit::AppNavigation.new(label: "Primary navigation") do |navigation|
|
|
392
|
+
navigation.body do
|
|
393
|
+
navigation.section(label: "Workspace") do
|
|
394
|
+
navigation.item("Overview", href: root_path, icon: :house, current: true)
|
|
395
|
+
navigation.item("Projects", href: projects_path, icon: :folder, badge: 12)
|
|
396
|
+
end
|
|
397
|
+
navigation.spacer
|
|
398
|
+
navigation.item("Settings", href: settings_path, icon: :settings)
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
shell.topbar do
|
|
404
|
+
render NitroKit::Button.new("New project", href: new_project_path, variant: :primary)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
shell.main { render @page }
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Change only `layout:` to `:topbar` or `:hybrid`; the same `brand`, `navigation`, `topbar`, and `main` declarations remain valid. Nitro owns the responsive breakpoint, narrow drawer, focus management, sticky regions, and one reflowed navigation DOM tree. Do not clone navigation for mobile or add route registries to the shell.
|
|
415
|
+
|
|
416
|
+
The gallery has complete executable examples for [sidebar](/gallery/compositions/application-sidebar), [topbar](/gallery/compositions/application-topbar), and [hybrid](/gallery/compositions/application-hybrid) applications. Each route contains multiple populated, empty, loading, long-content, missing-content, or error combinations.
|
|
417
|
+
|
|
418
|
+
## Rails forms and Hotwire
|
|
419
|
+
|
|
420
|
+
Customization does not change Rails ownership. Keep using Rails helpers directly from Phlex and select the Nitro builder explicitly:
|
|
421
|
+
|
|
422
|
+
```ruby
|
|
423
|
+
class ProfileForm < Phlex::HTML
|
|
424
|
+
include Phlex::Rails::Helpers::FormWith
|
|
425
|
+
|
|
426
|
+
def initialize(profile)
|
|
427
|
+
@profile = profile
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def view_template
|
|
431
|
+
form_with(model: @profile, builder: NitroKit::FormBuilder) do |form|
|
|
432
|
+
form.group do
|
|
433
|
+
form.field(:name, required: true)
|
|
434
|
+
form.field(:timezone, as: :select, options: timezone_choices)
|
|
435
|
+
form.submit("Save profile")
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Nitro Kit's engine contributes its controller pins automatically when `importmap-rails` is present. The application still owns Stimulus and its normal loader:
|
|
443
|
+
|
|
444
|
+
```js
|
|
445
|
+
// app/javascript/controllers/index.js
|
|
446
|
+
import { application } from "controllers/application";
|
|
447
|
+
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading";
|
|
448
|
+
|
|
449
|
+
eagerLoadControllersFrom("controllers", application);
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
This registers Nitro's `controllers/nk/*` modules together with application controllers. Without importmap, Ruby and CSS still work, but a bundler-based application must expose and register those Stimulus modules itself. Nitro Kit ships no third-party JavaScript runtime, and Nitro Kit 2.0 has no JavaScript-package entrypoint.
|
|
453
|
+
|
|
454
|
+
## Optional Tailwind CSS v4 adapter
|
|
455
|
+
|
|
456
|
+
Nitro Kit does not require Tailwind, Tailwind configuration, or Tailwind Preflight — it ships its own global preflight in the `nitro-kit.reset` cascade layer, which unlayered application CSS always overrides. The optional `nitro_kit-tailwind-v4.css` asset only establishes compatible cascade-layer order and maps Nitro tokens to common Tailwind v4 theme variables, including background, foreground, primary, destructive, radii, shadows, fonts, spacing, and transition defaults.
|
|
457
|
+
|
|
458
|
+
Tailwind remains compiled and configured by the application. An application can add further aliases in its Tailwind CSS source with the v4 CSS-first API:
|
|
459
|
+
|
|
460
|
+
```css
|
|
461
|
+
@import "tailwindcss";
|
|
462
|
+
|
|
463
|
+
@theme inline {
|
|
464
|
+
--color-brand: var(--nk-color-primary);
|
|
465
|
+
--color-brand-foreground: var(--nk-color-primary-foreground);
|
|
466
|
+
--font-product: var(--nk-font-sans);
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Use `@theme inline` when a Tailwind theme variable references another custom property so generated utilities resolve the live Nitro value. The adapter does not make Tailwind a Nitro runtime dependency, configure source detection, generate utility classes, or permit Tailwind classes inside Nitro component APIs.
|
|
471
|
+
|
|
472
|
+
## Public token reference
|
|
473
|
+
|
|
474
|
+
The following 84 variables are the complete public token set. Theme-independent tokens are declared on `:root`. Appearance tokens have light, dark, and system-fallback values. Derived tokens have defaults expressed in terms of other public tokens and remain overrideable.
|
|
475
|
+
|
|
476
|
+
### Typography
|
|
477
|
+
|
|
478
|
+
| Token | Role |
|
|
479
|
+
| --------------------------- | ---------------------------------- |
|
|
480
|
+
| `--nk-font-sans` | Default UI font family. |
|
|
481
|
+
| `--nk-font-mono` | Monospace font family. |
|
|
482
|
+
| `--nk-text-xs` | Extra-small text size. |
|
|
483
|
+
| `--nk-text-sm` | Small text size. |
|
|
484
|
+
| `--nk-text-base` | Base text size. |
|
|
485
|
+
| `--nk-text-lg` | Large text size. |
|
|
486
|
+
| `--nk-text-xl` | Extra-large text size. |
|
|
487
|
+
| `--nk-text-2xl` | Largest shipped display text size. |
|
|
488
|
+
| `--nk-leading-tight` | Tight line-height ratio. |
|
|
489
|
+
| `--nk-leading-normal` | Default line-height ratio. |
|
|
490
|
+
| `--nk-leading-relaxed` | Relaxed line-height ratio. |
|
|
491
|
+
| `--nk-font-weight-normal` | Normal text weight. |
|
|
492
|
+
| `--nk-font-weight-medium` | Medium text weight. |
|
|
493
|
+
| `--nk-font-weight-semibold` | Semibold text weight. |
|
|
494
|
+
| `--nk-font-weight-bold` | Bold text weight. |
|
|
495
|
+
| `--nk-typeset-font-body` | Typeset body font family. |
|
|
496
|
+
| `--nk-typeset-font-heading` | Typeset heading font family. |
|
|
497
|
+
| `--nk-typeset-font-mono` | Typeset code font family. |
|
|
498
|
+
| `--nk-typeset-size` | Typeset base text size. |
|
|
499
|
+
| `--nk-typeset-leading` | Typeset body line-height ratio. |
|
|
500
|
+
| `--nk-typeset-flow` | Typeset vertical rhythm unit. |
|
|
501
|
+
|
|
502
|
+
### Spacing and dimensions
|
|
503
|
+
|
|
504
|
+
| Token | Role |
|
|
505
|
+
| ------------------------ | --------------------------------------------------------------- |
|
|
506
|
+
| `--nk-space` | Base spacing unit multiplied throughout components and layouts. |
|
|
507
|
+
| `--nk-control-height-xs` | Extra-small control height. |
|
|
508
|
+
| `--nk-control-height-sm` | Small control height. |
|
|
509
|
+
| `--nk-control-height-md` | Default control height. |
|
|
510
|
+
| `--nk-control-height-lg` | Large control height. |
|
|
511
|
+
| `--nk-control-height-xl` | Extra-large control height. |
|
|
512
|
+
| `--nk-content-sm` | Small Container maximum width. |
|
|
513
|
+
| `--nk-content-md` | Medium Container maximum width. |
|
|
514
|
+
| `--nk-content-lg` | Large Container maximum width. |
|
|
515
|
+
| `--nk-content-xl` | Extra-large Container maximum width. |
|
|
516
|
+
|
|
517
|
+
### Shape, borders, and focus
|
|
518
|
+
|
|
519
|
+
| Token | Role |
|
|
520
|
+
| ------------------- | ----------------------------------- |
|
|
521
|
+
| `--nk-radius-xs` | Extra-small corner radius. |
|
|
522
|
+
| `--nk-radius-sm` | Small corner radius. |
|
|
523
|
+
| `--nk-radius-md` | Default control corner radius. |
|
|
524
|
+
| `--nk-radius-lg` | Large surface corner radius. |
|
|
525
|
+
| `--nk-radius-xl` | Extra-large overlay corner radius. |
|
|
526
|
+
| `--nk-radius-full` | Fully rounded pills and circles. |
|
|
527
|
+
| `--nk-border-width` | Default border and separator width. |
|
|
528
|
+
| `--nk-focus-width` | Focus-ring width. |
|
|
529
|
+
| `--nk-focus-offset` | Focus-ring offset. |
|
|
530
|
+
|
|
531
|
+
### Elevation and motion
|
|
532
|
+
|
|
533
|
+
| Token | Role |
|
|
534
|
+
| ---------------------- | ------------------------------------- |
|
|
535
|
+
| `--nk-shadow-sm` | Low surface elevation. |
|
|
536
|
+
| `--nk-shadow-md` | Medium floating elevation. |
|
|
537
|
+
| `--nk-shadow-lg` | High overlay elevation. |
|
|
538
|
+
| `--nk-duration-fast` | Fast interaction duration. |
|
|
539
|
+
| `--nk-duration-normal` | Default interaction duration. |
|
|
540
|
+
| `--nk-duration-slow` | Deliberate overlay or image duration. |
|
|
541
|
+
| `--nk-ease` | Default transition timing function. |
|
|
542
|
+
|
|
543
|
+
### Semantic colors
|
|
544
|
+
|
|
545
|
+
| Token | Role |
|
|
546
|
+
| ------------------------------- | ---------------------------------------------------------------------- |
|
|
547
|
+
| `--nk-color-canvas` | Page canvas and overscroll; light zinc uses a zinc-25-like near-white. |
|
|
548
|
+
| `--nk-color-surface` | Default component surface. |
|
|
549
|
+
| `--nk-color-surface-hover` | Derived interactive surface hover. |
|
|
550
|
+
| `--nk-color-elevated` | Raised or inset-neutral surface. |
|
|
551
|
+
| `--nk-color-foreground` | Primary text and icon color. |
|
|
552
|
+
| `--nk-color-muted` | Quiet fill. |
|
|
553
|
+
| `--nk-color-muted-foreground` | Secondary text and icon color. |
|
|
554
|
+
| `--nk-color-border` | Borders and separators. |
|
|
555
|
+
| `--nk-color-focus` | Focus indicator. |
|
|
556
|
+
| `--nk-color-primary` | Primary action and selected-state fill. |
|
|
557
|
+
| `--nk-color-primary-hover` | Derived primary interaction hover. |
|
|
558
|
+
| `--nk-color-primary-foreground` | Content placed on primary fill. |
|
|
559
|
+
| `--nk-color-neutral` | Neutral status fill or marker. |
|
|
560
|
+
| `--nk-color-neutral-content` | Strong neutral status content. |
|
|
561
|
+
| `--nk-color-info` | Informational status fill or marker. |
|
|
562
|
+
| `--nk-color-info-content` | Informational status content. |
|
|
563
|
+
| `--nk-color-success` | Successful status fill or marker. |
|
|
564
|
+
| `--nk-color-success-content` | Successful status content. |
|
|
565
|
+
| `--nk-color-warning` | Warning status fill or marker. |
|
|
566
|
+
| `--nk-color-warning-content` | Warning status content. |
|
|
567
|
+
| `--nk-color-danger` | Destructive action and error fill. |
|
|
568
|
+
| `--nk-color-danger-hover` | Derived destructive interaction hover. |
|
|
569
|
+
| `--nk-color-danger-foreground` | Content placed on destructive fill. |
|
|
570
|
+
| `--nk-color-danger-content` | Error and destructive status content. |
|
|
571
|
+
| `--nk-color-overlay` | Modal and drawer backdrop. |
|
|
572
|
+
|
|
573
|
+
### Default button colors
|
|
574
|
+
|
|
575
|
+
| Token | Role |
|
|
576
|
+
| -------------------------------------- | --------------------------------------- |
|
|
577
|
+
| `--nk-button-default-background` | Raised default-action fill. |
|
|
578
|
+
| `--nk-button-default-hover-background` | Raised default-action interaction fill. |
|
|
579
|
+
| `--nk-button-default-foreground` | Content on a raised default action. |
|
|
580
|
+
| `--nk-button-default-border` | Border around a raised default action. |
|
|
581
|
+
|
|
582
|
+
The native file-input selector uses the same treatment. Ordinary inputs, selects, textareas, unchecked controls, cards, dialogs, and menus continue to use the general surface tokens.
|
|
583
|
+
|
|
584
|
+
### Application shell
|
|
585
|
+
|
|
586
|
+
| Token | Role |
|
|
587
|
+
| ------------------------------------------ | --------------------------------------------------------------------- |
|
|
588
|
+
| `--nk-app-shell-sidebar-width` | Desktop sidebar width. |
|
|
589
|
+
| `--nk-app-shell-topbar-height` | Desktop and compact-header height. |
|
|
590
|
+
| `--nk-app-shell-background` | Shell canvas; derived from `--nk-color-canvas`. |
|
|
591
|
+
| `--nk-app-shell-sidebar-background` | Sidebar surface; derived from `--nk-color-surface`. |
|
|
592
|
+
| `--nk-app-shell-sidebar-foreground` | Sidebar content; derived from `--nk-color-foreground`. |
|
|
593
|
+
| `--nk-app-shell-sidebar-accent` | Current and hovered navigation fill; derived from `--nk-color-muted`. |
|
|
594
|
+
| `--nk-app-shell-sidebar-accent-foreground` | Content on the sidebar accent. |
|
|
595
|
+
| `--nk-app-shell-border` | Shell chrome separators; derived from `--nk-color-border`. |
|
data/docs/hotwire.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Hotwire with Nitro Kit
|
|
2
|
+
|
|
3
|
+
Nitro Kit owns component markup, CSS, and its focused progressive controllers.
|
|
4
|
+
Rails owns records, routes, authorization, queries, DOM identity, and server
|
|
5
|
+
responses. Hotwire transports server-rendered HTML between those boundaries.
|
|
6
|
+
|
|
7
|
+
## Choose the smallest interaction
|
|
8
|
+
|
|
9
|
+
1. Use ordinary links and forms under Turbo Drive.
|
|
10
|
+
2. Use a Turbo Frame for one independently navigable or replaceable region.
|
|
11
|
+
3. Return request-scoped Turbo Streams when one action changes multiple
|
|
12
|
+
regions.
|
|
13
|
+
4. Broadcast only when another session needs the update.
|
|
14
|
+
5. Add application Stimulus only for browser-owned behavior the preceding
|
|
15
|
+
layers cannot express.
|
|
16
|
+
|
|
17
|
+
Do not copy Nitro Kit controllers into the application. Do not add files under
|
|
18
|
+
`app/javascript/controllers/nk`; consume the `nk--*` controllers packaged by
|
|
19
|
+
the installed gem.
|
|
20
|
+
|
|
21
|
+
## Response matrix
|
|
22
|
+
|
|
23
|
+
| Request | Success | Invalid or denied |
|
|
24
|
+
| -------------------- | --------------------------------------------- | ------------------------------------------------------------------- |
|
|
25
|
+
| HTML form mutation | Redirect with `303 See Other` | Render HTML with `422` |
|
|
26
|
+
| Frame form mutation | Redirect or render the same frame ID | Render the same frame ID with `422` |
|
|
27
|
+
| Stream form mutation | Return only when multiple targets must change | Render the invalid form target with `422` and keep an HTML fallback |
|
|
28
|
+
| GET query | Render from URL parameters | Render a useful empty or error state |
|
|
29
|
+
|
|
30
|
+
Use `dom_id` or one named constant for a frame. The show, edit, invalid,
|
|
31
|
+
success, and cancel responses must preserve that identifier.
|
|
32
|
+
|
|
33
|
+
## Forms and application Stimulus
|
|
34
|
+
|
|
35
|
+
Let Turbo submit real Rails forms. Use `data-turbo-submits-with` to opt a Button
|
|
36
|
+
into submission feedback without replacing its visible label. The Button dims
|
|
37
|
+
immediately; `submission_indicator: :spinner` additionally reveals an indicator
|
|
38
|
+
after 1 second and lets the Button expand to fit it. Use `data-turbo-confirm` for
|
|
39
|
+
compact destructive confirmation.
|
|
40
|
+
Reviewed destructive flows may compose `DangerZone` and `Dialog`, but the
|
|
41
|
+
dialog must still submit a real Rails form.
|
|
42
|
+
|
|
43
|
+
Keep application controllers small, declarative, and disposable. For a
|
|
44
|
+
self-submitting control, the complete controller can be:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { Controller } from "@hotwired/stimulus";
|
|
48
|
+
|
|
49
|
+
export default class extends Controller {
|
|
50
|
+
submit() {
|
|
51
|
+
this.element.requestSubmit();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Attach `data-action="change->auto-submit#submit"` to the form so change events
|
|
57
|
+
bubble to that one controller root. Keep a submit button inside `noscript` as
|
|
58
|
+
the HTML fallback. Use `data-action` instead of manually registering DOM listeners. If a
|
|
59
|
+
controller owns a listener, observer, timer, object URL, or third-party
|
|
60
|
+
instance, release it in `disconnect`.
|
|
61
|
+
|
|
62
|
+
## Morphing and cache lifecycle
|
|
63
|
+
|
|
64
|
+
Default to morphable server-rendered HTML. Use `data-turbo-permanent` only for
|
|
65
|
+
a genuinely stateful island, always with a stable unique `id`. Update content
|
|
66
|
+
inside a permanent element deliberately instead of making broad page regions
|
|
67
|
+
permanent.
|
|
68
|
+
|
|
69
|
+
Clean ephemeral UI before Turbo caches the page. Nitro Kit controllers own
|
|
70
|
+
their own cache and reconnect behavior; application controllers must do the
|
|
71
|
+
same for application-owned state.
|
|
72
|
+
|
|
73
|
+
## Frame escape and recovery
|
|
74
|
+
|
|
75
|
+
An authentication redirect or error response inside a frame must not strand
|
|
76
|
+
the user behind a missing-frame error. Use `_top` when navigation must leave
|
|
77
|
+
the frame. Keep authentication and authorization handling capable of returning
|
|
78
|
+
a full-page response, and handle `turbo:frame-missing` only when the
|
|
79
|
+
application has a deliberate recovery policy.
|
|
80
|
+
|
|
81
|
+
Give lazy frames meaningful loading content. A failed frame should leave an
|
|
82
|
+
understandable state and a path to retry or continue without JavaScript.
|
|
83
|
+
|
|
84
|
+
## Verify behavior
|
|
85
|
+
|
|
86
|
+
- Request-test `303`, `422`, HTML fallback, and stable frame IDs.
|
|
87
|
+
- On mutation responses, assert submitted content inside the response's
|
|
88
|
+
matching frame, not merely somewhere in the response body.
|
|
89
|
+
- System-test focus, dialogs, frame navigation, and multi-target changes.
|
|
90
|
+
- Use Capybara assertions that wait for the DOM; never use `sleep`.
|
|
91
|
+
- Test navigation, morphing, and reconnection without duplicating controller
|
|
92
|
+
roots or listeners.
|
|
93
|
+
|
|
94
|
+
Read the matching recipe under `docs/patterns/` for complete compositions.
|