nitro_kit 2.0.0.alpha.3 → 2.0.0.alpha.4

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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +179 -0
  3. data/README.md +22 -26
  4. data/STYLE_GUIDE.md +10 -0
  5. data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +3 -3
  6. data/app/assets/stylesheets/nitro_kit.css +1220 -572
  7. data/app/components/nitro_kit/accordion.rb +1 -19
  8. data/app/components/nitro_kit/alert.rb +1 -11
  9. data/app/components/nitro_kit/app_navigation.rb +44 -9
  10. data/app/components/nitro_kit/appearance_picker.rb +10 -1
  11. data/app/components/nitro_kit/badge.rb +13 -4
  12. data/app/components/nitro_kit/combobox.rb +1 -17
  13. data/app/components/nitro_kit/command_palette.rb +1 -0
  14. data/app/components/nitro_kit/dropzone.rb +1 -1
  15. data/app/components/nitro_kit/form_builder.rb +1 -1
  16. data/app/components/nitro_kit/select.rb +1 -14
  17. data/app/components/nitro_kit/tabs.rb +1 -0
  18. data/app/javascript/controllers/nk/command_palette_controller.js +4 -0
  19. data/app/javascript/controllers/nk/tabs_controller.js +13 -1
  20. data/docs/agent_guide.md +63 -138
  21. data/docs/agent_native_spec.md +72 -400
  22. data/docs/browser_support.md +77 -188
  23. data/docs/component_contracts.md +56 -36
  24. data/docs/customization.md +675 -89
  25. data/docs/hotwire.md +61 -99
  26. data/docs/initialization_prompt.md +27 -57
  27. data/docs/migration_1_to_2.md +34 -18
  28. data/docs/patterns/application_foundation.md +45 -121
  29. data/docs/patterns/crud_resource.md +44 -129
  30. data/docs/patterns/destructive_action.md +45 -102
  31. data/docs/patterns/flash_and_toast.md +18 -36
  32. data/docs/patterns/inline_edit.md +21 -44
  33. data/docs/patterns/queryable_collection.md +65 -161
  34. data/docs/patterns/resource_form.md +33 -81
  35. data/docs/rails_conventions.md +38 -78
  36. data/docs/rails_integration.md +96 -474
  37. data/lib/nitro_kit/installation.rb +11 -3
  38. data/lib/nitro_kit/migration_inventory.rb +15 -8
  39. data/lib/nitro_kit/version.rb +1 -1
  40. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +7 -0
  41. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +15 -8
  42. data/src/stylesheets/nitro_kit/components/accordion.css +4 -4
  43. data/src/stylesheets/nitro_kit/components/alert.css +20 -29
  44. data/src/stylesheets/nitro_kit/components/app_navigation.css +74 -2
  45. data/src/stylesheets/nitro_kit/components/app_shell.css +3 -3
  46. data/src/stylesheets/nitro_kit/components/appearance_picker.css +20 -8
  47. data/src/stylesheets/nitro_kit/components/auth_shell.css +2 -2
  48. data/src/stylesheets/nitro_kit/components/avatar.css +12 -8
  49. data/src/stylesheets/nitro_kit/components/avatar_stack.css +12 -10
  50. data/src/stylesheets/nitro_kit/components/badge.css +13 -9
  51. data/src/stylesheets/nitro_kit/components/button.css +42 -25
  52. data/src/stylesheets/nitro_kit/components/card.css +2 -2
  53. data/src/stylesheets/nitro_kit/components/checkbox.css +29 -46
  54. data/src/stylesheets/nitro_kit/components/checkbox_group.css +3 -3
  55. data/src/stylesheets/nitro_kit/components/combobox.css +6 -6
  56. data/src/stylesheets/nitro_kit/components/command_palette.css +62 -4
  57. data/src/stylesheets/nitro_kit/components/container.css +6 -6
  58. data/src/stylesheets/nitro_kit/components/control_group.css +9 -9
  59. data/src/stylesheets/nitro_kit/components/danger_zone.css +6 -6
  60. data/src/stylesheets/nitro_kit/components/data_section.css +4 -4
  61. data/src/stylesheets/nitro_kit/components/details_table.css +7 -5
  62. data/src/stylesheets/nitro_kit/components/dialog.css +3 -3
  63. data/src/stylesheets/nitro_kit/components/dropdown.css +7 -7
  64. data/src/stylesheets/nitro_kit/components/dropzone.css +7 -7
  65. data/src/stylesheets/nitro_kit/components/empty_state.css +4 -4
  66. data/src/stylesheets/nitro_kit/components/field.css +2 -2
  67. data/src/stylesheets/nitro_kit/components/fieldset.css +2 -2
  68. data/src/stylesheets/nitro_kit/components/flex.css +1 -1
  69. data/src/stylesheets/nitro_kit/components/grid.css +1 -1
  70. data/src/stylesheets/nitro_kit/components/icon.css +5 -5
  71. data/src/stylesheets/nitro_kit/components/input.css +33 -6
  72. data/src/stylesheets/nitro_kit/components/page_header.css +4 -4
  73. data/src/stylesheets/nitro_kit/components/pagination.css +3 -3
  74. data/src/stylesheets/nitro_kit/components/pagination_bar.css +2 -2
  75. data/src/stylesheets/nitro_kit/components/palette.css +140 -72
  76. data/src/stylesheets/nitro_kit/components/progressive_image.css +1 -1
  77. data/src/stylesheets/nitro_kit/components/radio_button.css +28 -33
  78. data/src/stylesheets/nitro_kit/components/radio_button_group.css +6 -6
  79. data/src/stylesheets/nitro_kit/components/rich_text_area.css +2 -2
  80. data/src/stylesheets/nitro_kit/components/select.css +24 -8
  81. data/src/stylesheets/nitro_kit/components/settings_layout.css +5 -5
  82. data/src/stylesheets/nitro_kit/components/settings_section.css +2 -2
  83. data/src/stylesheets/nitro_kit/components/sheet.css +10 -10
  84. data/src/stylesheets/nitro_kit/components/stat_grid.css +1 -1
  85. data/src/stylesheets/nitro_kit/components/switch.css +34 -40
  86. data/src/stylesheets/nitro_kit/components/table.css +8 -4
  87. data/src/stylesheets/nitro_kit/components/tabs.css +5 -1
  88. data/src/stylesheets/nitro_kit/components/textarea.css +20 -5
  89. data/src/stylesheets/nitro_kit/components/toast.css +11 -58
  90. data/src/stylesheets/nitro_kit/components/toolbar.css +2 -2
  91. data/src/stylesheets/nitro_kit/components/tooltip.css +4 -0
  92. data/src/stylesheets/nitro_kit/components/typeset.css +4 -4
  93. data/src/stylesheets/nitro_kit/reset.css +6 -6
  94. data/src/stylesheets/nitro_kit/tokens.css +497 -74
  95. metadata +1 -2
  96. data/docs/new_app_strategy.md +0 -22
@@ -106,25 +106,7 @@ module NitroKit
106
106
  end
107
107
 
108
108
  def chevron
109
- svg(
110
- **slot_attributes(
111
- :icon,
112
- attributes: {
113
- viewbox: "0 0 16 16",
114
- width: 16,
115
- height: 16,
116
- fill: "none",
117
- stroke: "currentColor",
118
- stroke_width: 1.5,
119
- stroke_linecap: "round",
120
- stroke_linejoin: "round",
121
- focusable: "false",
122
- aria: { hidden: true }
123
- }
124
- )
125
- ) do |svg|
126
- svg.path(d: "m4 6 4 4 4-4")
127
- end
109
+ render_in_slot(Icon.new(:chevron_down, size: :sm), :icon)
128
110
  end
129
111
 
130
112
  def trigger_id(item)
@@ -2,17 +2,7 @@
2
2
 
3
3
  module NitroKit
4
4
  class Alert < Component
5
- VARIANTS = %i[default info success warning error].freeze
6
-
7
- # The palette family each semantic variant borrows in `palette.css`.
8
- # Alert owns this mapping; it is not the Badge color axis.
9
- VARIANT_PALETTE = {
10
- default: :zinc,
11
- info: :blue,
12
- success: :green,
13
- warning: :amber,
14
- error: :red
15
- }.freeze
5
+ VARIANTS = %i[default info success warning destructive].freeze
16
6
 
17
7
  LIVE_MODES = %i[off polite assertive].freeze
18
8
 
@@ -5,8 +5,8 @@ module NitroKit
5
5
  alias_method :html_header, :header
6
6
  alias_method :html_footer, :footer
7
7
 
8
- Item = ::Data.define(:text, :href, :icon, :badge, :current, :html, :aria, :data, :css_class)
9
- Section = ::Data.define(:label, :entries)
8
+ Item = ::Data.define(:text, :href, :icon, :icon_end, :badge, :current, :html, :aria, :data, :css_class)
9
+ Section = ::Data.define(:label, :entries, :collapsible, :expanded)
10
10
  Entry = ::Data.define(:kind)
11
11
 
12
12
  def initialize(
@@ -78,10 +78,16 @@ module NitroKit
78
78
  nil
79
79
  end
80
80
 
81
- def section(label: nil, &content)
81
+ def section(label: nil, collapsible: false, expanded: true, &content)
82
82
  ensure_phase!(:body, :section)
83
83
  raise ArgumentError, "AppNavigation section requires a block" unless content
84
84
 
85
+ collapsible = validate_boolean!(:collapsible, collapsible)
86
+ expanded = validate_boolean!(:expanded, expanded)
87
+ if collapsible && label.nil?
88
+ raise ArgumentError, "AppNavigation collapsible section requires a label"
89
+ end
90
+
85
91
  entries = []
86
92
  previous_entries = @entry_target
87
93
  previous_phase = @phase
@@ -94,7 +100,12 @@ module NitroKit
94
100
  unless entries.any?(Item)
95
101
  raise ArgumentError, "AppNavigation section requires at least one item"
96
102
  end
97
- previous_entries << Section.new(label: validate_optional_text!(:label, label), entries:)
103
+ previous_entries << Section.new(
104
+ label: validate_optional_text!(:label, label),
105
+ entries:,
106
+ collapsible:,
107
+ expanded:
108
+ )
98
109
  nil
99
110
  ensure
100
111
  @entry_target = previous_entries
@@ -106,6 +117,7 @@ module NitroKit
106
117
  text,
107
118
  href:,
108
119
  icon: nil,
120
+ icon_end: nil,
109
121
  badge: nil,
110
122
  badge_color: :neutral,
111
123
  current: false,
@@ -118,6 +130,7 @@ module NitroKit
118
130
  text = validate_text!(:text, text)
119
131
  href = validate_text!(:href, href)
120
132
  icon = item_icon(icon)
133
+ icon_end = item_icon(icon_end, name: :icon_end)
121
134
  badge = item_badge(badge, badge_color)
122
135
  current = validate_boolean!(:current, current)
123
136
 
@@ -130,6 +143,7 @@ module NitroKit
130
143
  text:,
131
144
  href:,
132
145
  icon:,
146
+ icon_end:,
133
147
  badge:,
134
148
  current:,
135
149
  html:,
@@ -205,13 +219,33 @@ module NitroKit
205
219
 
206
220
  def render_section(entry)
207
221
  li(**slot_attributes(:section)) do
208
- span(**slot_attributes(:section_label)) { plain(entry.label) } if entry.label
209
- ul(**slot_attributes(:section_list, attributes: section_list_attributes(entry))) do
210
- entry.entries.each { |child| render_entry(child) }
222
+ if entry.collapsible
223
+ details(
224
+ **slot_attributes(:section_disclosure, attributes: { open: entry.expanded })
225
+ ) do
226
+ summary(**slot_attributes(:section_label)) do
227
+ plain(entry.label)
228
+ section_chevron
229
+ end
230
+ render_section_list(entry)
231
+ end
232
+ else
233
+ span(**slot_attributes(:section_label)) { plain(entry.label) } if entry.label
234
+ render_section_list(entry)
211
235
  end
212
236
  end
213
237
  end
214
238
 
239
+ def render_section_list(entry)
240
+ ul(**slot_attributes(:section_list, attributes: section_list_attributes(entry))) do
241
+ entry.entries.each { |child| render_entry(child) }
242
+ end
243
+ end
244
+
245
+ def section_chevron
246
+ render_in_slot(Icon.new(:chevron_down, size: :xs), :section_icon)
247
+ end
248
+
215
249
  def section_list_attributes(entry)
216
250
  entry.label ? { aria: { label: entry.label } } : {}
217
251
  end
@@ -234,6 +268,7 @@ module NitroKit
234
268
  render_in_slot(entry.icon, :item_icon) if entry.icon
235
269
  span(**slot_attributes(:item_label)) { plain(entry.text) }
236
270
  render_in_slot(entry.badge, :item_badge) if entry.badge
271
+ render_in_slot(entry.icon_end, :item_icon_end) if entry.icon_end
237
272
  end
238
273
  end
239
274
  end
@@ -277,10 +312,10 @@ module NitroKit
277
312
  validate_text!(name, value)
278
313
  end
279
314
 
280
- def item_icon(value)
315
+ def item_icon(value, name: :icon)
281
316
  return if value.nil?
282
317
  unless (value.is_a?(String) || value.is_a?(Symbol)) && !value.to_s.strip.empty?
283
- raise ArgumentError, "icon must be a non-blank String or Symbol"
318
+ raise ArgumentError, "#{name} must be a non-blank String or Symbol"
284
319
  end
285
320
 
286
321
  Icon.new(value, size: :sm)
@@ -13,6 +13,7 @@ module NitroKit
13
13
  def initialize(
14
14
  id:,
15
15
  label: I18n.t("nitro_kit.appearance_picker.label"),
16
+ label_visible: true,
16
17
  presentation: :segmented,
17
18
  preference: :system,
18
19
  html: {},
@@ -22,13 +23,21 @@ module NitroKit
22
23
  )
23
24
  @identifier = validate_id!("AppearancePicker id", id)
24
25
  @label = validate_label!(label)
26
+ @label_visible = validate_boolean!(:label_visible, label_visible)
25
27
  @presentation = validate_choice!(:presentation, presentation, PRESENTATIONS)
26
28
  @preference = validate_choice!(:preference, preference, PREFERENCES)
29
+ if !@label_visible && @presentation != :segmented
30
+ raise ArgumentError,
31
+ "AppearancePicker label_visible: false requires the segmented presentation"
32
+ end
27
33
 
28
34
  super(
29
35
  component: :appearance_picker,
30
36
  attributes: {
31
37
  id: @identifier,
38
+ # Browsers give legends special layout, so a hidden label omits the
39
+ # element entirely and the fieldset keeps its name through ARIA.
40
+ aria: @label_visible ? {} : { label: @label },
32
41
  data: {
33
42
  controller: "nk--appearance",
34
43
  presentation: @presentation,
@@ -50,7 +59,7 @@ module NitroKit
50
59
  return render_dropdown if @presentation == :dropdown
51
60
 
52
61
  fieldset(**root_attributes) do
53
- legend(**slot_attributes(:legend)) { plain(@label) }
62
+ legend(**slot_attributes(:legend)) { plain(@label) } if @label_visible
54
63
  div(**slot_attributes(:options)) do
55
64
  PREFERENCES.each { |preference| render_option(preference) }
56
65
  end
@@ -4,16 +4,25 @@ module NitroKit
4
4
  class Badge < Component
5
5
  VARIANTS = %i[default outline].freeze
6
6
  SIZES = %i[xs sm md].freeze
7
- COLORS = %i[
8
- zinc red orange amber yellow lime green emerald teal cyan sky blue indigo
9
- violet purple fuchsia pink rose neutral info success warning danger
7
+ # Two axes on one option, both public and both themeable.
8
+ #
9
+ # The semantic families follow the `--nk-palette-{family}` tint roles, so
10
+ # they move with an application's brand: retheme `--nk-palette-destructive` and
11
+ # every `destructive` badge follows. The decorative hues follow the
12
+ # `--nk-palette-{hue}` roles and stay the color they name, for categorical
13
+ # labelling where meaning is not the point.
14
+ SEMANTIC_COLORS = %i[neutral info success warning destructive].freeze
15
+ PALETTE_COLORS = %i[
16
+ red orange amber yellow lime green emerald teal cyan sky blue indigo
17
+ violet purple fuchsia pink rose
10
18
  ].freeze
19
+ COLORS = (PALETTE_COLORS + SEMANTIC_COLORS).freeze
11
20
 
12
21
  def initialize(
13
22
  text = nil,
14
23
  variant: :default,
15
24
  size: :md,
16
- color: :zinc,
25
+ color: :neutral,
17
26
  id: nil,
18
27
  html: {},
19
28
  aria: {},
@@ -117,23 +117,7 @@ module NitroKit
117
117
  :input
118
118
  )
119
119
 
120
- svg(
121
- **slot_attributes(
122
- :icon,
123
- attributes: {
124
- viewbox: "0 0 16 16",
125
- width: 16,
126
- height: 16,
127
- fill: "none",
128
- stroke: "currentColor",
129
- stroke_width: 1.5,
130
- stroke_linecap: "round",
131
- stroke_linejoin: "round",
132
- focusable: "false",
133
- aria: { hidden: true }
134
- }
135
- )
136
- ) { |svg| svg.path(d: "m4 6 4 4 4-4") }
120
+ render_in_slot(Icon.new(:chevron_down, size: :sm), :icon)
137
121
  end
138
122
  end
139
123
 
@@ -254,6 +254,7 @@ module NitroKit
254
254
  nk__dialog_target: "panel",
255
255
  nk__command_palette_target: "panel",
256
256
  action: [
257
+ "command->nk--command-palette#guardOpen",
257
258
  "click->nk--dialog#dismiss",
258
259
  "cancel->nk--dialog#cancel",
259
260
  "close->nk--dialog#restoreFocus",
@@ -44,7 +44,7 @@ module NitroKit
44
44
  name:,
45
45
  label: I18n.t("nitro_kit.dropzone.label"),
46
46
  description: nil,
47
- presentation: :input,
47
+ presentation: :minimal,
48
48
  direct_upload: true,
49
49
  multiple: false,
50
50
  accept: nil,
@@ -115,7 +115,7 @@ module NitroKit
115
115
  id: nil,
116
116
  label: I18n.t("nitro_kit.dropzone.label"),
117
117
  description: nil,
118
- presentation: :input,
118
+ presentation: :minimal,
119
119
  direct_upload: true,
120
120
  multiple: false,
121
121
  accept: nil,
@@ -77,20 +77,7 @@ module NitroKit
77
77
  def render_toggle_icon
78
78
  return if @multiple
79
79
 
80
- svg(
81
- **slot_attributes(
82
- :icon,
83
- attributes: {
84
- viewbox: "0 0 20 20",
85
- fill: "none",
86
- stroke: "currentColor",
87
- stroke_width: 1.5,
88
- aria: { hidden: true }
89
- }
90
- )
91
- ) do |icon|
92
- icon.path(d: "m6.5 8 3.5 3.5L13.5 8", stroke_linecap: "round", stroke_linejoin: "round")
93
- end
80
+ render_in_slot(Icon.new(:chevron_down, size: :sm), :icon)
94
81
  end
95
82
 
96
83
  def control_attributes
@@ -140,6 +140,7 @@ module NitroKit
140
140
  data: {
141
141
  key: tab.key,
142
142
  state: active ? "active" : "inactive",
143
+ action: "beforematch->nk--tabs#reveal",
143
144
  nk__tabs_target: "panel"
144
145
  }
145
146
  }
@@ -68,6 +68,10 @@ export default class extends Controller {
68
68
  this.openTimer = setTimeout(() => this.#prepareOpenPanel(), 0);
69
69
  }
70
70
 
71
+ guardOpen(event) {
72
+ if (event.command === "show-modal" && !this.canOpen) event.preventDefault();
73
+ }
74
+
71
75
  shortcut(event) {
72
76
  if (
73
77
  event.defaultPrevented ||
@@ -42,6 +42,14 @@ export default class extends Controller {
42
42
  }
43
43
  }
44
44
 
45
+ reveal(event) {
46
+ const tab = this.tabTargets.find(
47
+ (candidate) => candidate.dataset.key === event.currentTarget.dataset.key,
48
+ );
49
+
50
+ if (tab && !tab.disabled) this.activeValue = tab.dataset.key;
51
+ }
52
+
45
53
  navigate(event) {
46
54
  const rightToLeft = getComputedStyle(this.element).direction === "rtl";
47
55
  const directionKeys =
@@ -143,11 +151,15 @@ export default class extends Controller {
143
151
 
144
152
  this.panelTargets.forEach((panel) => {
145
153
  const active = panel.dataset.key === value;
154
+ const tab = this.tabTargets.find(
155
+ (candidate) => candidate.dataset.key === panel.dataset.key,
156
+ );
146
157
 
147
- panel.hidden = !active;
148
158
  if (active) {
159
+ panel.removeAttribute("hidden");
149
160
  panel.setAttribute("aria-hidden", "false");
150
161
  } else {
162
+ panel.setAttribute("hidden", tab?.disabled ? "" : "until-found");
151
163
  panel.removeAttribute("aria-hidden");
152
164
  }
153
165
  panel.dataset.state = active ? "active" : "inactive";
data/docs/agent_guide.md CHANGED
@@ -1,8 +1,10 @@
1
- # Building applications with Nitro Kit
1
+ # Nitro Kit application guide for coding agents
2
2
 
3
- This is the routing guide for coding agents working in a Rails application that uses Nitro Kit. It describes how to find the installed contract, select components, and apply the same Rails and Hotwire conventions across features.
3
+ **Audience:** Coding agents changing a Rails application that uses Nitro Kit.
4
+ Humans should start with the [README](../README.md) and
5
+ [Rails integration guide](rails_integration.md).
4
6
 
5
- ## Start from the installed version
7
+ ## Resolve the installed version
6
8
 
7
9
  From the application root, run:
8
10
 
@@ -10,145 +12,68 @@ From the application root, run:
10
12
  bundle show nitro_kit
11
13
  ```
12
14
 
13
- Read this guide from that directory, then consult only the reference needed for the task:
14
-
15
- | Task | Read next |
16
- | ------------------------------------------- | ----------------------------------------- |
17
- | Rails models, routes, CRUD, and tests | `docs/rails_conventions.md` |
18
- | Authentication, teams, shell, and settings | `docs/patterns/application_foundation.md` |
19
- | Build a complete product resource | `docs/patterns/crud_resource.md` |
20
- | Choose or compose UI | `docs/component_contracts.md` |
21
- | Rails assets, forms, and application shells | `docs/rails_integration.md` |
22
- | Turbo, Frames, Streams, morphs, or Stimulus | `docs/hotwire.md` |
23
- | Browser support or progressive fallback | `docs/browser_support.md` |
24
- | Theme tokens and application composition | `docs/customization.md` |
25
- | Upgrade an existing Nitro Kit 1.x app | `docs/migration_1_to_2.md` |
26
- | Query, sort, filter, or paginate | `docs/patterns/queryable_collection.md` |
27
- | Create, update, and show validation | `docs/patterns/resource_form.md` |
28
- | Delete, revoke, archive, or confirm | `docs/patterns/destructive_action.md` |
29
- | Flash messages and notifications | `docs/patterns/flash_and_toast.md` |
30
- | Edit and cancel inside a page | `docs/patterns/inline_edit.md` |
31
-
32
- The installed component source is the final authority for constructor and compound-method details. This guide describes Nitro Kit 2.x. Do not use an API remembered from Nitro Kit 1.x or another installed version.
33
-
34
- ## Select the highest-level matching component
35
-
36
- Prefer the component that owns the whole region, then compose smaller components inside it.
37
-
38
- | Product need | Begin with |
39
- | -------------------------------- | ------------------------------------------------------------- |
40
- | Application chrome | `AppShell`, `AppNavigation`, `CommandPalette` |
41
- | Authentication page | `AuthShell` |
42
- | Settings navigation | `SettingsLayout` |
43
- | Team or account administration | `AppShell`, `Toolbar`, `Table`, `SettingsSection` |
44
- | App page title and basic actions | `AppShell`, then `Toolbar` |
45
- | Content-led page introduction | `PageHeader` |
46
- | Data region | `DataSection`, then `Table` or `EmptyState` |
47
- | Queryable tabular data | `Table`, `Toolbar`, `PaginationBar` |
48
- | Model-backed form | `SettingsSection`, Rails `form_with`, `NitroKit::FormBuilder` |
49
- | Destructive settings | `DangerZone`, optionally `Dialog` |
50
- | Transient server feedback | `Toast::FlashMessages` |
51
- | Rendered Markdown or rich text | `Container`, then `Typeset` |
52
- | General grouping | `Card`, `Flex`, `Grid`, `Container` |
53
-
54
- Application-specific product UI belongs under the application's namespace and composes Nitro components. Nitro owns component markup, styles, accessibility structure, and narrowly scoped progressive behavior. The application owns product policy, records, routes, authorization, queries, DOM IDs, and server responses.
55
-
56
- For admin CRUD, treat the starter and entitled flows as complete compositions:
57
- use toolbar Back links on child routes, keep deletion on edit, keep status in
58
- the details flow, implement full invitation and membership management, and use
59
- native links for settings destinations. Do not stop after rendering a screen
60
- that only looks structurally similar.
61
-
62
- ## Preserve controls during migration
63
-
64
- Before changing a 1.x application, read `docs/migration_1_to_2.md`. Inventory
65
- user-facing flows and behavior before mapping helpers. When the Nitro Kit MCP
66
- catalog is available, search by product workflow rather than old component
67
- name, then prefer the highest-level matching composition before replacing
68
- atoms.
69
-
70
- After establishing that inventory, generate the host integration smoke tests
71
- from the currently bundled gem with `bin/rails generate
72
- nitro_kit:upgrade_smoke_tests`. The generator preserves existing files and
73
- skips missing Rails Minitest or system-test infrastructure with setup guidance.
74
- Keep the generated explanatory header in each test: these are host-integration
75
- regression checks, not substitutes for application-specific acceptance tests.
76
- Run supported generated tests before and after the application conversion,
77
- while retaining application-specific acceptance coverage for every inventoried
78
- flow. Customize each generated `prepare_nitro_kit_upgrade_smoke_test` method
79
- with host sign-in and account-selection helpers when `ApplicationController`
80
- requires them; do not disable the host callbacks.
81
-
82
- In an existing application, replace a form control only when the installed
83
- Nitro Kit catalog provides a genuine semantic and behavioral equivalent.
84
- Preserve its parameter name, IDs, values, errors, accessibility, uploads, and
85
- browser behavior. If no equivalent exists, keep or re-express the control as
86
- application-owned Rails and semantic HTML, optionally inside a custom
87
- `form.field` composition. Never downgrade an editor, autocomplete, date range,
88
- upload, or other specialized input to the nearest generic Nitro control merely
89
- for visual consistency.
90
-
91
- Do not retain copied Nitro Kit 1.x source as the fallback. Remove the legacy
92
- component and preserve the unsupported behavior in clearly application-owned
93
- code. Report the missing equivalent as a Nitro Kit coverage gap.
94
-
95
- At the end of a migration, search the whole application for
96
- `desperately_need_a_class:` and review every result. Aim for zero rather than
97
- translating old utility classes mechanically. Move application layout and
98
- visual treatment to wrappers, use semantic component options or native
99
- attributes, accept incidental Nitro defaults, and keep unmatched product UI
100
- application-owned. Remove generic class forwarding from shared builders.
101
- Retain an escape only for a named external integration that requires a class
102
- hook, and record that reason in the migration review.
103
-
104
- ## Use one interaction grammar
105
-
106
- Choose the smallest primitive that completes the interaction:
107
-
108
- 1. Ordinary Rails links and forms under Turbo Drive.
109
- 2. A Turbo Frame for one independently navigable or replaceable region.
110
- 3. A request-scoped Turbo Stream response when one action changes multiple regions.
111
- 4. A broadcast only when other sessions must receive the change.
112
- 5. Application Stimulus only for browser-only behavior the preceding layers cannot express.
113
-
114
- Successful non-GET HTML submissions redirect with `303 See Other`. Invalid form submissions render the same invalid model with `422 Unprocessable Entity`. GET parameters remain the source of truth for queryable collections. Frame identifiers come from `dom_id` or one named constant shared by rendering, responses, and tests.
115
-
116
- ## Stay inside the public boundary
15
+ Read this guide from that directory. The installed
16
+ [component contracts](component_contracts.md) define the public API.
17
+
18
+ | Task | Read |
19
+ | -------------------------------------------------- | ------------------------------------------------------------ |
20
+ | Rails models, routes, CRUD, and tests | [Rails conventions](rails_conventions.md) |
21
+ | Authentication, teams, shell, and settings | [Application foundation](patterns/application_foundation.md) |
22
+ | Complete product CRUD | [CRUD resource](patterns/crud_resource.md) |
23
+ | Components and composition | [Component contracts](component_contracts.md) |
24
+ | Installation, assets, forms, and Rails integration | [Rails integration](rails_integration.md) |
25
+ | Turbo, Frames, Streams, morphs, or Stimulus | [Hotwire](hotwire.md) |
26
+ | Browser compatibility or fallback behavior | [Browser support](browser_support.md) |
27
+ | Theme tokens and application CSS | [Customization](customization.md) |
28
+ | Nitro Kit 1.x upgrade | [Migration guide](migration_1_to_2.md) |
29
+ | Query, sort, filter, or paginate | [Queryable collection](patterns/queryable_collection.md) |
30
+ | Create, update, and validation | [Resource form](patterns/resource_form.md) |
31
+ | Delete, revoke, archive, or confirm | [Destructive action](patterns/destructive_action.md) |
32
+ | Flash messages and notifications | [Flash and toast](patterns/flash_and_toast.md) |
33
+ | Edit and cancel inside a page | [Inline edit](patterns/inline_edit.md) |
34
+
35
+ Before changing a Nitro Kit 1.x application, read the migration guide. Do not
36
+ infer 2.x APIs from memory or old application code.
37
+
38
+ ## Preserve the application's architecture
39
+
40
+ For a greenfield application, run:
41
+
42
+ ```sh
43
+ bin/rails generate phlex:install
44
+ ```
45
+
46
+ Use Phlex for the application layout, route views, and reusable UI. In an established
47
+ application, preserve its view architecture and introduce Phlex only at the
48
+ requested boundary. Do not perform an application-wide migration unless it is
49
+ explicitly authorized.
50
+
51
+ ## Stay within the public API
117
52
 
118
53
  - Include `NitroKit` once in the application's base Phlex component and use
119
- capitalized Kit methods such as `Button(...)` and `Card(...)`.
120
- - Use the scoped `NitroKit::Button(...)` form when inclusion is not appropriate.
121
- - Use an explicit constructor such as `Button.new(...)` only when another API
122
- needs a component object; Kit methods render immediately.
123
- - Select `NitroKit::FormBuilder` explicitly from Rails `form_with`.
124
- - Put a standalone form's visible fields, submit control, and related links
125
- inside `form.group`; hidden fields may remain before the group.
126
- - Parents own rhythm. Wrap stacked fields in `FieldGroup` as
127
- `render NitroKit::FieldGroup.new { ... }`, or `form.group` inside a
128
- `form_with` block. Two Fields, or a Field and its submit Button, rendered as
129
- bare siblings of a `form` or a plain `div` have no gap between them at all.
130
- Use `Flex` or `Grid` instead only when the arrangement is deliberately
131
- inline or multi-column.
132
- - Use component options, compound declarations, layouts, and documented `--nk-*` theme properties.
133
- - Keep an HTML fallback for every Turbo form flow.
134
- - Treat that HTML response as a Turbo transport fallback, not automatically a
135
- JavaScript-free interaction; use `docs/browser_support.md` for the canonical
136
- component classifications.
137
- - Test semantic output and stable owned attributes.
138
-
139
- Do not copy component source, add `nk_*` helpers, invent a general ERB bridge, mutate Nitro-owned Stimulus controllers, or pass `class:` and `style:`. The intentionally loud `desperately_need_a_class:` escape exists only for external integrations that require a class hook.
140
-
141
- ## Application `AGENTS.md`
142
-
143
- Install the durable project instruction and local skills from the consuming
144
- application root:
54
+ Kit methods such as `Button(...)` and `Card(...)`.
55
+ - Use `NitroKit::Button(...)` when inclusion is inappropriate and `.new` only
56
+ when another API requires a component object.
57
+ - Select `NitroKit::FormBuilder` explicitly with Rails `form_with`.
58
+ - Put stacked fields and actions in `form.group` or `FieldGroup`.
59
+ - Use documented component options, compound methods, native attributes, and
60
+ public `--nk-*` tokens.
61
+ - Keep product policy, records, routes, authorization, queries, DOM IDs, and
62
+ server responses in application code.
63
+ - Do not copy Nitro source, add `nk_*` helpers, mutate Nitro controllers, or
64
+ pass `class:` or `style:`. Use `desperately_need_a_class:` only for a named
65
+ external integration that requires a class hook.
66
+
67
+ Follow the canonical [browser support policy](browser_support.md) for
68
+ JavaScript and fallback guarantees.
69
+
70
+ ## Install project-local guidance
71
+
72
+ From the consuming application, run:
145
73
 
146
74
  ```sh
147
75
  bin/rails generate nitro_kit:install
148
76
  ```
149
77
 
150
- The installer preserves application-owned content around a bounded Nitro Kit 2
151
- block in `AGENTS.md` and adds project-local Rails, Hotwire, and UI skills for
152
- supported agents. The skills deliberately resolve the installed gem first, so
153
- upgrading the gem upgrades the instructions they use. Re-run the generator
154
- after an upgrade; it never copies component or controller source.
78
+ The installer updates its managed `AGENTS.md` block and local Nitro Kit skills
79
+ without copying component source. Re-run it after upgrading the gem.