nitro_kit 2.0.0.alpha.2 → 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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +229 -1
  3. data/README.md +25 -10
  4. data/STYLE_GUIDE.md +47 -5
  5. data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +3 -3
  6. data/app/assets/stylesheets/nitro_kit.css +2820 -1816
  7. data/app/components/nitro_kit/accordion.rb +2 -26
  8. data/app/components/nitro_kit/alert.rb +1 -11
  9. data/app/components/nitro_kit/app_navigation.rb +45 -11
  10. data/app/components/nitro_kit/app_shell.rb +1 -7
  11. data/app/components/nitro_kit/appearance_picker.rb +11 -12
  12. data/app/components/nitro_kit/avatar.rb +6 -1
  13. data/app/components/nitro_kit/avatar_stack.rb +3 -0
  14. data/app/components/nitro_kit/badge.rb +13 -4
  15. data/app/components/nitro_kit/button_group.rb +2 -2
  16. data/app/components/nitro_kit/checkbox.rb +7 -4
  17. data/app/components/nitro_kit/checkbox_group.rb +0 -25
  18. data/app/components/nitro_kit/combobox.rb +4 -33
  19. data/app/components/nitro_kit/command_palette.rb +42 -59
  20. data/app/components/nitro_kit/component.rb +57 -5
  21. data/app/components/nitro_kit/danger_zone.rb +5 -1
  22. data/app/components/nitro_kit/data_section.rb +28 -4
  23. data/app/components/nitro_kit/details_table.rb +19 -8
  24. data/app/components/nitro_kit/dialog.rb +23 -19
  25. data/app/components/nitro_kit/dropdown.rb +1 -6
  26. data/app/components/nitro_kit/dropzone.rb +4 -15
  27. data/app/components/nitro_kit/empty_state.rb +21 -2
  28. data/app/components/nitro_kit/field.rb +35 -11
  29. data/app/components/nitro_kit/form_builder.rb +1 -2
  30. data/app/components/nitro_kit/progressive_image.rb +2 -2
  31. data/app/components/nitro_kit/radio_button.rb +7 -4
  32. data/app/components/nitro_kit/radio_button_group.rb +0 -25
  33. data/app/components/nitro_kit/select.rb +4 -17
  34. data/app/components/nitro_kit/settings_layout.rb +17 -4
  35. data/app/components/nitro_kit/{form_section.rb → settings_section.rb} +18 -11
  36. data/app/components/nitro_kit/sheet.rb +29 -20
  37. data/app/components/nitro_kit/stat_grid.rb +18 -2
  38. data/app/components/nitro_kit/table.rb +22 -3
  39. data/app/components/nitro_kit/tabs.rb +2 -13
  40. data/app/components/nitro_kit/toast.rb +4 -2
  41. data/app/components/nitro_kit/toolbar.rb +2 -0
  42. data/app/components/nitro_kit/tooltip.rb +7 -10
  43. data/app/javascript/controllers/nk/app_shell_controller.js +3 -3
  44. data/app/javascript/controllers/nk/appearance_controller.js +6 -0
  45. data/app/javascript/controllers/nk/combobox_controller.js +51 -1
  46. data/app/javascript/controllers/nk/command_palette_controller.js +25 -11
  47. data/app/javascript/controllers/nk/dialog_controller.js +54 -0
  48. data/app/javascript/controllers/nk/dropdown_controller.js +45 -0
  49. data/app/javascript/controllers/nk/dropzone_controller.js +2 -2
  50. data/app/javascript/controllers/nk/progressive_image_controller.js +11 -7
  51. data/app/javascript/controllers/nk/tabs_controller.js +21 -2
  52. data/app/javascript/controllers/nk/tooltip_controller.js +9 -0
  53. data/config/locales/en.yml +4 -1
  54. data/docs/agent_guide.md +63 -123
  55. data/docs/agent_native_spec.md +72 -383
  56. data/docs/browser_support.md +82 -0
  57. data/docs/component_contracts.md +108 -82
  58. data/docs/customization.md +689 -91
  59. data/docs/hotwire.md +50 -76
  60. data/docs/initialization_prompt.md +26 -31
  61. data/docs/migration_1_to_2.md +88 -30
  62. data/docs/patterns/application_foundation.md +45 -119
  63. data/docs/patterns/crud_resource.md +44 -126
  64. data/docs/patterns/destructive_action.md +45 -77
  65. data/docs/patterns/flash_and_toast.md +18 -36
  66. data/docs/patterns/inline_edit.md +21 -44
  67. data/docs/patterns/queryable_collection.md +65 -161
  68. data/docs/patterns/resource_form.md +33 -81
  69. data/docs/rails_conventions.md +38 -75
  70. data/docs/rails_integration.md +96 -446
  71. data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +12 -0
  72. data/lib/nitro_kit/installation.rb +20 -7
  73. data/lib/nitro_kit/migration_inventory.rb +275 -8
  74. data/lib/nitro_kit/upgrade_smoke_test.rb +10 -12
  75. data/lib/nitro_kit/version.rb +1 -1
  76. data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +10 -3
  77. data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +4 -0
  78. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +13 -1
  79. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +32 -15
  80. data/src/stylesheets/nitro_kit/components/accordion.css +44 -44
  81. data/src/stylesheets/nitro_kit/components/alert.css +27 -36
  82. data/src/stylesheets/nitro_kit/components/app_navigation.css +92 -20
  83. data/src/stylesheets/nitro_kit/components/app_shell.css +199 -196
  84. data/src/stylesheets/nitro_kit/components/appearance_picker.css +61 -39
  85. data/src/stylesheets/nitro_kit/components/auth_shell.css +2 -2
  86. data/src/stylesheets/nitro_kit/components/avatar.css +15 -11
  87. data/src/stylesheets/nitro_kit/components/avatar_stack.css +48 -29
  88. data/src/stylesheets/nitro_kit/components/badge.css +13 -9
  89. data/src/stylesheets/nitro_kit/components/button.css +121 -79
  90. data/src/stylesheets/nitro_kit/components/button_group.css +19 -14
  91. data/src/stylesheets/nitro_kit/components/card.css +12 -6
  92. data/src/stylesheets/nitro_kit/components/checkbox.css +54 -67
  93. data/src/stylesheets/nitro_kit/components/checkbox_group.css +16 -16
  94. data/src/stylesheets/nitro_kit/components/combobox.css +39 -29
  95. data/src/stylesheets/nitro_kit/components/command_palette.css +110 -51
  96. data/src/stylesheets/nitro_kit/components/container.css +6 -6
  97. data/src/stylesheets/nitro_kit/components/control_group.css +61 -67
  98. data/src/stylesheets/nitro_kit/components/danger_zone.css +8 -8
  99. data/src/stylesheets/nitro_kit/components/data_section.css +4 -4
  100. data/src/stylesheets/nitro_kit/components/details_table.css +17 -15
  101. data/src/stylesheets/nitro_kit/components/dialog.css +8 -5
  102. data/src/stylesheets/nitro_kit/components/dropdown.css +36 -30
  103. data/src/stylesheets/nitro_kit/components/dropzone.css +78 -35
  104. data/src/stylesheets/nitro_kit/components/empty_state.css +7 -7
  105. data/src/stylesheets/nitro_kit/components/field.css +32 -28
  106. data/src/stylesheets/nitro_kit/components/field_group.css +13 -0
  107. data/src/stylesheets/nitro_kit/components/fieldset.css +8 -2
  108. data/src/stylesheets/nitro_kit/components/flex.css +1 -1
  109. data/src/stylesheets/nitro_kit/components/grid.css +1 -1
  110. data/src/stylesheets/nitro_kit/components/icon.css +5 -5
  111. data/src/stylesheets/nitro_kit/components/input.css +42 -8
  112. data/src/stylesheets/nitro_kit/components/layout.css +165 -165
  113. data/src/stylesheets/nitro_kit/components/page_header.css +7 -7
  114. data/src/stylesheets/nitro_kit/components/pagination.css +58 -49
  115. data/src/stylesheets/nitro_kit/components/pagination_bar.css +9 -9
  116. data/src/stylesheets/nitro_kit/components/palette.css +145 -77
  117. data/src/stylesheets/nitro_kit/components/progressive_image.css +45 -55
  118. data/src/stylesheets/nitro_kit/components/radio_button.css +48 -46
  119. data/src/stylesheets/nitro_kit/components/radio_button_group.css +49 -49
  120. data/src/stylesheets/nitro_kit/components/rich_text_area.css +16 -1
  121. data/src/stylesheets/nitro_kit/components/select.css +24 -15
  122. data/src/stylesheets/nitro_kit/components/settings_layout.css +26 -23
  123. data/src/stylesheets/nitro_kit/components/settings_section.css +84 -0
  124. data/src/stylesheets/nitro_kit/components/sheet.css +30 -10
  125. data/src/stylesheets/nitro_kit/components/stat_grid.css +11 -10
  126. data/src/stylesheets/nitro_kit/components/switch.css +60 -62
  127. data/src/stylesheets/nitro_kit/components/table.css +43 -35
  128. data/src/stylesheets/nitro_kit/components/tabs.css +48 -40
  129. data/src/stylesheets/nitro_kit/components/textarea.css +20 -5
  130. data/src/stylesheets/nitro_kit/components/toast.css +11 -47
  131. data/src/stylesheets/nitro_kit/components/toolbar.css +16 -41
  132. data/src/stylesheets/nitro_kit/components/tooltip.css +38 -29
  133. data/src/stylesheets/nitro_kit/components/typeset.css +150 -4
  134. data/src/stylesheets/nitro_kit/reset.css +15 -15
  135. data/src/stylesheets/nitro_kit/tokens.css +501 -79
  136. metadata +4 -4
  137. data/docs/new_app_strategy.md +0 -22
  138. data/src/stylesheets/nitro_kit/components/form_section.css +0 -71
@@ -50,6 +50,7 @@ export default class extends Controller {
50
50
  }
51
51
 
52
52
  connect() {
53
+ this.outsidePointerDown = this.outsidePointerDown.bind(this);
53
54
  this.activeOption = null;
54
55
  this.enhanced = true;
55
56
  this.nativeRequired = this.requiredValue;
@@ -64,6 +65,7 @@ export default class extends Controller {
64
65
 
65
66
  disconnect() {
66
67
  this.stopPositioning?.();
68
+ this.stopOutsideDismissal();
67
69
  this.enhanced = false;
68
70
  const control = this.element.querySelector(
69
71
  ':scope > [data-slot="combobox-control"]',
@@ -143,6 +145,17 @@ export default class extends Controller {
143
145
  open() {
144
146
  if (this.inputTarget.disabled) return;
145
147
 
148
+ // A committed selection is replaced, not appended to: opening selects the
149
+ // visible text so the first keystroke starts a fresh filter.
150
+ if (!this.openValue && this.inputTarget.value !== "") {
151
+ this.inputTarget.select();
152
+ this.inputTarget.addEventListener(
153
+ "mouseup",
154
+ (event) => event.preventDefault(),
155
+ { once: true },
156
+ );
157
+ }
158
+
146
159
  this.openValue = true;
147
160
  }
148
161
 
@@ -151,10 +164,30 @@ export default class extends Controller {
151
164
  this.setActive(null);
152
165
  }
153
166
 
154
- closeFromOutside(event) {
167
+ // Capture-phase document pointerdown mirrors the Dropdown controller: iOS
168
+ // Safari does not reliably deliver window click events for outside taps.
169
+ outsidePointerDown(event) {
155
170
  if (!this.element.contains(event.target)) this.close();
156
171
  }
157
172
 
173
+ startOutsideDismissal() {
174
+ if (this.stopOutsideDismissalListener) return;
175
+
176
+ document.addEventListener("pointerdown", this.outsidePointerDown, true);
177
+ this.stopOutsideDismissalListener = () => {
178
+ document.removeEventListener(
179
+ "pointerdown",
180
+ this.outsidePointerDown,
181
+ true,
182
+ );
183
+ this.stopOutsideDismissalListener = null;
184
+ };
185
+ }
186
+
187
+ stopOutsideDismissal() {
188
+ this.stopOutsideDismissalListener?.();
189
+ }
190
+
158
191
  filter() {
159
192
  const query = this.inputTarget.value.trim().toLocaleLowerCase();
160
193
  let exactMatch = null;
@@ -268,9 +301,11 @@ export default class extends Controller {
268
301
 
269
302
  if (open) {
270
303
  this.startPositioning();
304
+ this.startOutsideDismissal();
271
305
  } else {
272
306
  this.stopPositioning?.();
273
307
  this.stopPositioning = null;
308
+ this.stopOutsideDismissal();
274
309
  }
275
310
  }
276
311
 
@@ -351,6 +386,21 @@ export default class extends Controller {
351
386
 
352
387
  this.statusTarget.textContent =
353
388
  count === 0 ? this.noResultsMessage : this.resultsMessage(count);
389
+ this.toggleEmptyState(count === 0);
390
+ }
391
+
392
+ toggleEmptyState(empty) {
393
+ if (empty && !this.emptyElement) {
394
+ const item = document.createElement("li");
395
+ item.setAttribute("role", "presentation");
396
+ item.setAttribute("data-slot", "combobox-empty");
397
+ item.textContent = this.noResultsMessage;
398
+ this.listboxTarget.append(item);
399
+ this.emptyElement = item;
400
+ } else if (!empty && this.emptyElement) {
401
+ this.emptyElement.remove();
402
+ this.emptyElement = null;
403
+ }
354
404
  }
355
405
 
356
406
  get visibleOptions() {
@@ -22,13 +22,13 @@ export default class extends Controller {
22
22
  this.connected = true;
23
23
  this.restoreFocus = true;
24
24
  this.loadedQuery = this.inputTarget.value;
25
- this.triggerTarget.removeAttribute("command");
26
25
  this.searchTarget.hidden = false;
27
26
  this.element.dataset.enhanced = "true";
28
27
  }
29
28
 
30
29
  disconnect() {
31
30
  this.connected = false;
31
+ clearTimeout(this.openTimer);
32
32
  clearTimeout(this.searchTimer);
33
33
  const trigger = this.element.querySelector(
34
34
  ':scope > [data-slot="command-palette-trigger"]',
@@ -51,21 +51,25 @@ export default class extends Controller {
51
51
  });
52
52
  if (empty) empty.hidden = true;
53
53
  if (status) status.textContent = "";
54
- trigger?.setAttribute("command", "show-modal");
55
54
  if (search) search.hidden = true;
56
55
  delete this.element.dataset.enhanced;
57
56
  }
58
57
 
59
- open(event) {
60
- event?.preventDefault();
61
- if (!this.canOpen) return;
58
+ openedByTrigger(event) {
59
+ if (!this.canOpen) {
60
+ event.preventDefault();
61
+ event.stopPropagation();
62
+ return;
63
+ }
62
64
 
63
- this.returnFocus = document.activeElement;
65
+ this.returnFocus = event.currentTarget;
64
66
  this.restoreFocus = true;
65
- this.reset();
66
- this.panelTarget.showModal();
67
- this.inputTarget.focus();
68
- if (this.hasFormTarget) this.submitSearch();
67
+ clearTimeout(this.openTimer);
68
+ this.openTimer = setTimeout(() => this.#prepareOpenPanel(), 0);
69
+ }
70
+
71
+ guardOpen(event) {
72
+ if (event.command === "show-modal" && !this.canOpen) event.preventDefault();
69
73
  }
70
74
 
71
75
  shortcut(event) {
@@ -87,7 +91,9 @@ export default class extends Controller {
87
91
  if (!this.canOpen) return;
88
92
 
89
93
  event.preventDefault();
90
- this.open();
94
+ this.returnFocus = document.activeElement;
95
+ this.panelTarget.showModal();
96
+ this.#prepareOpenPanel();
91
97
  }
92
98
  }
93
99
 
@@ -246,4 +252,12 @@ export default class extends Controller {
246
252
 
247
253
  return !activeModal || activeModal.contains(this.element);
248
254
  }
255
+
256
+ #prepareOpenPanel() {
257
+ if (!this.panelTarget.open) return;
258
+
259
+ this.reset();
260
+ this.inputTarget.focus();
261
+ if (this.hasFormTarget) this.submitSearch();
262
+ }
249
263
  }
@@ -4,6 +4,43 @@ export default class extends Controller {
4
4
  static targets = ["panel"];
5
5
  static values = { dismissible: Boolean };
6
6
 
7
+ disconnect() {
8
+ this.returnFocus = null;
9
+ }
10
+
11
+ invoke(event) {
12
+ const invoker = event.target.closest(
13
+ "[command], [data-nk--dialog-command]",
14
+ );
15
+ if (!invoker || !this.element.contains(invoker) || invoker.disabled) return;
16
+
17
+ const command =
18
+ invoker.getAttribute("command") ||
19
+ invoker.getAttribute("data-nk--dialog-command");
20
+ if (!["show-modal", "close"].includes(command)) return;
21
+
22
+ const panel = this.#commandPanel(invoker);
23
+ if (!panel) return;
24
+ if (command === "show-modal") this.returnFocus = invoker;
25
+ if (this.#nativeRelationshipRuns(invoker, panel, command)) return;
26
+
27
+ event.preventDefault();
28
+ if (command === "show-modal" && !panel.open) panel.showModal();
29
+ if (command === "close" && panel.open) panel.close();
30
+ }
31
+
32
+ closeForCache() {
33
+ this.returnFocus = null;
34
+ if (this.panelTarget.open) this.panelTarget.close();
35
+ }
36
+
37
+ restoreFocus() {
38
+ const returnFocus = this.returnFocus;
39
+
40
+ this.returnFocus = null;
41
+ if (returnFocus?.isConnected) returnFocus.focus();
42
+ }
43
+
7
44
  dismiss(event) {
8
45
  if (!this.dismissibleValue || event.target !== this.panelTarget) return;
9
46
 
@@ -20,4 +57,21 @@ export default class extends Controller {
20
57
  cancel(event) {
21
58
  if (!this.dismissibleValue) event.preventDefault();
22
59
  }
60
+
61
+ #commandPanel(invoker) {
62
+ const targetId = invoker.getAttribute("commandfor");
63
+ if (targetId) {
64
+ const target = document.getElementById(targetId);
65
+ if (target instanceof HTMLDialogElement && this.element.contains(target))
66
+ return target;
67
+ }
68
+
69
+ return invoker.hasAttribute("data-nk--dialog-command")
70
+ ? this.panelTarget
71
+ : null;
72
+ }
73
+
74
+ #nativeRelationshipRuns(invoker, panel, command) {
75
+ return invoker.commandForElement === panel && invoker.command === command;
76
+ }
23
77
  }
@@ -7,8 +7,13 @@ import {
7
7
  export default class extends Controller {
8
8
  static targets = ["trigger", "content", "item"];
9
9
 
10
+ connect() {
11
+ this.outsidePointerDown = this.outsidePointerDown.bind(this);
12
+ }
13
+
10
14
  disconnect() {
11
15
  this.stopPositioning?.();
16
+ this.stopOutsidePointerFallback();
12
17
  }
13
18
 
14
19
  openFromKeyboard(event) {
@@ -36,10 +41,12 @@ export default class extends Controller {
36
41
  focusOpened(event) {
37
42
  if (event.newState === "open") {
38
43
  this.startPositioning();
44
+ this.startOutsidePointerFallback();
39
45
  this.focusInitialItem();
40
46
  } else {
41
47
  this.stopPositioning?.();
42
48
  this.stopPositioning = null;
49
+ this.stopOutsidePointerFallback();
43
50
  this.focusLast = false;
44
51
  this.restoreFocus();
45
52
  }
@@ -65,6 +72,37 @@ export default class extends Controller {
65
72
  this.stopPositioning = observeOverlayPosition(update);
66
73
  }
67
74
 
75
+ startOutsidePointerFallback() {
76
+ if (!this.supportsPopover || this.stopOutsidePointerFallbackListener)
77
+ return;
78
+
79
+ document.addEventListener("pointerdown", this.outsidePointerDown, true);
80
+ this.stopOutsidePointerFallbackListener = () => {
81
+ document.removeEventListener(
82
+ "pointerdown",
83
+ this.outsidePointerDown,
84
+ true,
85
+ );
86
+ this.stopOutsidePointerFallbackListener = null;
87
+ };
88
+ }
89
+
90
+ stopOutsidePointerFallback() {
91
+ this.stopOutsidePointerFallbackListener?.();
92
+ }
93
+
94
+ outsidePointerDown(event) {
95
+ if (!this.contentTarget.matches(":popover-open")) return;
96
+
97
+ const path = event.composedPath?.();
98
+ const insideMenu = path
99
+ ? path.includes(this.contentTarget) || path.includes(this.triggerTarget)
100
+ : this.contentTarget.contains(event.target) ||
101
+ this.triggerTarget.contains(event.target);
102
+
103
+ if (!insideMenu) this.hide();
104
+ }
105
+
68
106
  navigate(event) {
69
107
  switch (event.key) {
70
108
  case "Escape":
@@ -123,4 +161,11 @@ export default class extends Controller {
123
161
  (item) => !item.disabled && item.getAttribute("aria-disabled") !== "true",
124
162
  );
125
163
  }
164
+
165
+ get supportsPopover() {
166
+ return (
167
+ typeof this.contentTarget.showPopover === "function" &&
168
+ typeof this.contentTarget.hidePopover === "function"
169
+ );
170
+ }
126
171
  }
@@ -230,8 +230,8 @@ export default class extends Controller {
230
230
  this.element.dataset.state = this.initialInputDisabled
231
231
  ? "disabled"
232
232
  : selectedCount > 0
233
- ? "success"
234
- : "idle";
233
+ ? "success"
234
+ : "idle";
235
235
  }
236
236
 
237
237
  replaceFiles(files) {
@@ -24,6 +24,10 @@ export default class extends Controller {
24
24
  if (this.hasFallbackTarget) this.fallbackTarget.hidden = true;
25
25
  }
26
26
 
27
+ prepareForCache() {
28
+ delete this.element.dataset.enhanced;
29
+ }
30
+
27
31
  imageTargetConnected(image) {
28
32
  if (this.connected) this.bindImage(image);
29
33
  }
@@ -47,11 +51,14 @@ export default class extends Controller {
47
51
  this.releaseImage();
48
52
  this.boundImage = image;
49
53
  this.revision = (this.revision || 0) + 1;
50
- this.setState("loading");
51
54
  image.addEventListener("load", this.onLoad);
52
55
  image.addEventListener("error", this.onError);
53
56
 
54
- if (image.complete) this.reflectComplete(image);
57
+ if (image.complete) {
58
+ this.reflectComplete(image);
59
+ } else {
60
+ this.setState("loading");
61
+ }
55
62
  }
56
63
 
57
64
  releaseImage() {
@@ -98,11 +105,8 @@ export default class extends Controller {
98
105
  }
99
106
 
100
107
  reflectComplete(image) {
101
- if (image.naturalWidth > 0) {
102
- this.loaded();
103
- } else {
104
- this.failed();
105
- }
108
+ this.revision = (this.revision || 0) + 1;
109
+ this.setState(image.naturalWidth > 0 ? "loaded" : "error");
106
110
  }
107
111
 
108
112
  setState(state) {
@@ -42,10 +42,21 @@ 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) {
54
+ const rightToLeft = getComputedStyle(this.element).direction === "rtl";
46
55
  const directionKeys =
47
56
  this.orientationValue === "vertical"
48
57
  ? ["ArrowUp", "ArrowDown"]
58
+ : rightToLeft
59
+ ? ["ArrowRight", "ArrowLeft"]
49
60
  : ["ArrowLeft", "ArrowRight"];
50
61
 
51
62
  if (![...directionKeys, "Home", "End"].includes(event.key)) return;
@@ -140,9 +151,17 @@ export default class extends Controller {
140
151
 
141
152
  this.panelTargets.forEach((panel) => {
142
153
  const active = panel.dataset.key === value;
154
+ const tab = this.tabTargets.find(
155
+ (candidate) => candidate.dataset.key === panel.dataset.key,
156
+ );
143
157
 
144
- panel.hidden = !active;
145
- panel.setAttribute("aria-hidden", String(!active));
158
+ if (active) {
159
+ panel.removeAttribute("hidden");
160
+ panel.setAttribute("aria-hidden", "false");
161
+ } else {
162
+ panel.setAttribute("hidden", tab?.disabled ? "" : "until-found");
163
+ panel.removeAttribute("aria-hidden");
164
+ }
146
165
  panel.dataset.state = active ? "active" : "inactive";
147
166
  panel.tabIndex = active ? 0 : -1;
148
167
  });
@@ -6,6 +6,8 @@ export default class extends Controller {
6
6
  }
7
7
 
8
8
  dismiss(event) {
9
+ if (!this.shown) return;
10
+
9
11
  event.preventDefault();
10
12
  this.element.dataset.dismissed = "";
11
13
  }
@@ -21,4 +23,11 @@ export default class extends Controller {
21
23
  reset() {
22
24
  delete this.element.dataset.dismissed;
23
25
  }
26
+
27
+ get shown() {
28
+ return (
29
+ !("dismissed" in this.element.dataset) &&
30
+ this.element.matches(":hover, :focus-within")
31
+ );
32
+ }
24
33
  }
@@ -12,7 +12,9 @@ en:
12
12
  dark: "Dark"
13
13
  system: "System"
14
14
  avatar_stack:
15
- overflow: "%{count} more avatars"
15
+ overflow:
16
+ one: "%{count} more avatar"
17
+ other: "%{count} more avatars"
16
18
  combobox:
17
19
  invalid_selection: "Choose an option."
18
20
  no_results: "No options found."
@@ -37,6 +39,7 @@ en:
37
39
  dropzone:
38
40
  label: "Upload files"
39
41
  prompt: "Drop files here or choose them from your device."
42
+ compact_prompt: "Drop or choose files."
40
43
  preview_list: "Selected files"
41
44
  progress: "Upload progress"
42
45
  progress_for: "Upload progress for %{name}"
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,130 +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
- | Theme tokens and application composition | `docs/customization.md` |
24
- | Upgrade an existing Nitro Kit 1.x app | `docs/migration_1_to_2.md` |
25
- | Query, sort, filter, or paginate | `docs/patterns/queryable_collection.md` |
26
- | Create, update, and show validation | `docs/patterns/resource_form.md` |
27
- | Delete, revoke, archive, or confirm | `docs/patterns/destructive_action.md` |
28
- | Flash messages and notifications | `docs/patterns/flash_and_toast.md` |
29
- | Edit and cancel inside a page | `docs/patterns/inline_edit.md` |
30
-
31
- 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.
32
-
33
- ## Select the highest-level matching component
34
-
35
- Prefer the component that owns the whole region, then compose smaller components inside it.
36
-
37
- | Product need | Begin with |
38
- | -------------------------------- | --------------------------------------------------------- |
39
- | Application chrome | `AppShell`, `AppNavigation`, `CommandPalette` |
40
- | Authentication page | `AuthShell` |
41
- | Settings navigation | `SettingsLayout` |
42
- | Team or account administration | `AppShell`, `Toolbar`, `Table`, `FormSection` |
43
- | App page title and basic actions | `AppShell`, then `Toolbar` |
44
- | Content-led page introduction | `PageHeader` |
45
- | Data region | `DataSection`, then `Table` or `EmptyState` |
46
- | Queryable tabular data | `Table`, `Toolbar`, `PaginationBar` |
47
- | Model-backed form | `FormSection`, Rails `form_with`, `NitroKit::FormBuilder` |
48
- | Destructive settings | `DangerZone`, optionally `Dialog` |
49
- | Transient server feedback | `Toast::FlashMessages` |
50
- | Rendered Markdown or rich text | `Container`, then `Typeset` |
51
- | General grouping | `Card`, `Flex`, `Grid`, `Container` |
52
-
53
- 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.
54
-
55
- For admin CRUD, treat the starter and entitled flows as complete compositions:
56
- use toolbar Back links on child routes, keep deletion on edit, keep status in
57
- the details flow, implement full invitation and membership management, and use
58
- native links for settings destinations. Do not stop after rendering a screen
59
- that only looks structurally similar.
60
-
61
- ## Preserve controls during migration
62
-
63
- Before changing a 1.x application, read `docs/migration_1_to_2.md`. Inventory
64
- user-facing flows and behavior before mapping helpers. When the Nitro Kit MCP
65
- catalog is available, search by product workflow rather than old component
66
- name, then prefer the highest-level matching composition before replacing
67
- atoms.
68
-
69
- After establishing that inventory, generate the host integration smoke tests
70
- from the currently bundled gem with `bin/rails generate
71
- nitro_kit:upgrade_smoke_tests`. The generator preserves existing files and
72
- skips missing Rails Minitest or system-test infrastructure with setup guidance.
73
- Run supported generated tests before and after the application conversion,
74
- while retaining application-specific acceptance coverage for every inventoried
75
- flow. Customize each generated `prepare_nitro_kit_upgrade_smoke_test` method
76
- with host sign-in and account-selection helpers when `ApplicationController`
77
- requires them; do not disable the host callbacks.
78
-
79
- In an existing application, replace a form control only when the installed
80
- Nitro Kit catalog provides a genuine semantic and behavioral equivalent.
81
- Preserve its parameter name, IDs, values, errors, accessibility, uploads, and
82
- browser behavior. If no equivalent exists, keep or re-express the control as
83
- application-owned Rails and semantic HTML, optionally inside a custom
84
- `form.field` composition. Never downgrade an editor, autocomplete, date range,
85
- upload, or other specialized input to the nearest generic Nitro control merely
86
- for visual consistency.
87
-
88
- Do not retain copied Nitro Kit 1.x source as the fallback. Remove the legacy
89
- component and preserve the unsupported behavior in clearly application-owned
90
- code. Report the missing equivalent as a Nitro Kit coverage gap.
91
-
92
- ## Use one interaction grammar
93
-
94
- Choose the smallest primitive that completes the interaction:
95
-
96
- 1. Ordinary Rails links and forms under Turbo Drive.
97
- 2. A Turbo Frame for one independently navigable or replaceable region.
98
- 3. A request-scoped Turbo Stream response when one action changes multiple regions.
99
- 4. A broadcast only when other sessions must receive the change.
100
- 5. Application Stimulus only for browser-only behavior the preceding layers cannot express.
101
-
102
- 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.
103
-
104
- ## 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
105
52
 
106
53
  - Include `NitroKit` once in the application's base Phlex component and use
107
- capitalized Kit methods such as `Button(...)` and `Card(...)`.
108
- - Use the scoped `NitroKit::Button(...)` form when inclusion is not appropriate.
109
- - Use an explicit constructor such as `Button.new(...)` only when another API
110
- needs a component object; Kit methods render immediately.
111
- - Select `NitroKit::FormBuilder` explicitly from Rails `form_with`.
112
- - Put a standalone form's visible fields, submit control, and related links
113
- inside `form.group`; hidden fields may remain before the group.
114
- - Parents own rhythm. Wrap stacked fields in `FieldGroup` as
115
- `render NitroKit::FieldGroup.new { ... }`, or `form.group` inside a
116
- `form_with` block. Two Fields, or a Field and its submit Button, rendered as
117
- bare siblings of a `form` or a plain `div` have no gap between them at all.
118
- Use `Flex` or `Grid` instead only when the arrangement is deliberately
119
- inline or multi-column.
120
- - Use component options, compound declarations, layouts, and documented `--nk-*` theme properties.
121
- - Keep an HTML fallback for every Turbo form flow.
122
- - Test semantic output and stable owned attributes.
123
-
124
- 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.
125
-
126
- ## Application `AGENTS.md`
127
-
128
- Install the durable project instruction and local skills from the consuming
129
- 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:
130
73
 
131
74
  ```sh
132
75
  bin/rails generate nitro_kit:install
133
76
  ```
134
77
 
135
- The installer preserves application-owned content around a bounded Nitro Kit 2
136
- block in `AGENTS.md` and adds project-local Rails, Hotwire, and UI skills for
137
- supported agents. The skills deliberately resolve the installed gem first, so
138
- upgrading the gem upgrades the instructions they use. Re-run the generator
139
- 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.