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.
Files changed (219) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/plugins/marketplace.json +20 -0
  3. data/CHANGELOG.md +172 -0
  4. data/LICENSE +28 -0
  5. data/README.md +22 -15
  6. data/STYLE_GUIDE.md +425 -0
  7. data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
  8. data/app/assets/stylesheets/nitro_kit.css +7239 -0
  9. data/app/components/nitro_kit/accordion.rb +125 -65
  10. data/app/components/nitro_kit/alert.rb +80 -43
  11. data/app/components/nitro_kit/app_navigation.rb +307 -0
  12. data/app/components/nitro_kit/app_shell.rb +227 -0
  13. data/app/components/nitro_kit/appearance_bootstrap.rb +146 -0
  14. data/app/components/nitro_kit/appearance_picker.rb +156 -0
  15. data/app/components/nitro_kit/auth_shell.rb +34 -0
  16. data/app/components/nitro_kit/avatar.rb +85 -29
  17. data/app/components/nitro_kit/avatar_stack.rb +155 -9
  18. data/app/components/nitro_kit/badge.rb +43 -81
  19. data/app/components/nitro_kit/button.rb +180 -103
  20. data/app/components/nitro_kit/button_group.rb +63 -11
  21. data/app/components/nitro_kit/button_to.rb +69 -0
  22. data/app/components/nitro_kit/card.rb +42 -45
  23. data/app/components/nitro_kit/checkbox.rb +115 -62
  24. data/app/components/nitro_kit/checkbox_group.rb +131 -22
  25. data/app/components/nitro_kit/choice.rb +55 -0
  26. data/app/components/nitro_kit/combobox.rb +292 -105
  27. data/app/components/nitro_kit/command_palette.rb +412 -0
  28. data/app/components/nitro_kit/component.rb +322 -46
  29. data/app/components/nitro_kit/container.rb +32 -0
  30. data/app/components/nitro_kit/control_group.rb +45 -0
  31. data/app/components/nitro_kit/danger_zone.rb +78 -0
  32. data/app/components/nitro_kit/data_section.rb +106 -0
  33. data/app/components/nitro_kit/details_table.rb +229 -0
  34. data/app/components/nitro_kit/dialog.rb +257 -83
  35. data/app/components/nitro_kit/dropdown.rb +335 -122
  36. data/app/components/nitro_kit/dropzone.rb +326 -0
  37. data/app/components/nitro_kit/empty_state.rb +99 -0
  38. data/app/components/nitro_kit/field.rb +461 -236
  39. data/app/components/nitro_kit/field_group.rb +15 -8
  40. data/app/components/nitro_kit/fieldset.rb +37 -39
  41. data/app/components/nitro_kit/flex.rb +49 -0
  42. data/app/components/nitro_kit/form_builder.rb +313 -72
  43. data/app/components/nitro_kit/form_section.rb +79 -0
  44. data/app/components/nitro_kit/grid.rb +34 -0
  45. data/app/components/nitro_kit/icon.rb +54 -25
  46. data/app/components/nitro_kit/input.rb +97 -10
  47. data/app/components/nitro_kit/label.rb +22 -6
  48. data/app/components/nitro_kit/layout_options.rb +7 -0
  49. data/app/components/nitro_kit/page_header.rb +76 -0
  50. data/app/components/nitro_kit/pagination.rb +355 -69
  51. data/app/components/nitro_kit/pagination_bar.rb +90 -0
  52. data/app/components/nitro_kit/progressive_image.rb +181 -0
  53. data/app/components/nitro_kit/radio_button.rb +77 -55
  54. data/app/components/nitro_kit/radio_button_group.rb +107 -36
  55. data/app/components/nitro_kit/responsive_value.rb +94 -0
  56. data/app/components/nitro_kit/rich_text_area.rb +31 -0
  57. data/app/components/nitro_kit/select.rb +129 -64
  58. data/app/components/nitro_kit/settings_layout.rb +152 -0
  59. data/app/components/nitro_kit/sheet.rb +202 -0
  60. data/app/components/nitro_kit/stat_grid.rb +75 -0
  61. data/app/components/nitro_kit/switch.rb +108 -51
  62. data/app/components/nitro_kit/table.rb +237 -45
  63. data/app/components/nitro_kit/tabs.rb +168 -63
  64. data/app/components/nitro_kit/textarea.rb +81 -10
  65. data/app/components/nitro_kit/toast.rb +211 -80
  66. data/app/components/nitro_kit/toolbar.rb +75 -0
  67. data/app/components/nitro_kit/tooltip.rb +235 -34
  68. data/app/components/nitro_kit/typeset.rb +28 -0
  69. data/app/javascript/controllers/nk/app_shell_controller.js +224 -0
  70. data/app/javascript/controllers/nk/appearance_controller.js +73 -0
  71. data/app/javascript/controllers/nk/avatar_controller.js +15 -0
  72. data/app/javascript/controllers/nk/button_controller.js +56 -0
  73. data/app/javascript/controllers/nk/checkable_controller.js +41 -0
  74. data/app/javascript/controllers/nk/combobox_controller.js +314 -82
  75. data/app/javascript/controllers/nk/command_palette_controller.js +249 -0
  76. data/app/javascript/controllers/nk/dialog_controller.js +14 -10
  77. data/app/javascript/controllers/nk/dropdown_controller.js +106 -58
  78. data/app/javascript/controllers/nk/dropzone/direct_upload.js +61 -0
  79. data/app/javascript/controllers/nk/dropzone/file_rules.js +77 -0
  80. data/app/javascript/controllers/nk/dropzone/form_submit_lock.js +30 -0
  81. data/app/javascript/controllers/nk/dropzone_controller.js +482 -0
  82. data/app/javascript/controllers/nk/overlay_position.js +37 -0
  83. data/app/javascript/controllers/nk/progressive_image_controller.js +116 -0
  84. data/app/javascript/controllers/nk/tabs_controller.js +135 -23
  85. data/app/javascript/controllers/nk/toast_controller.js +112 -45
  86. data/app/javascript/controllers/nk/tooltip_controller.js +12 -47
  87. data/config/importmap.rb +2 -0
  88. data/config/locales/en.yml +98 -0
  89. data/docs/agent_guide.md +139 -0
  90. data/docs/agent_native_spec.md +422 -0
  91. data/docs/component_contracts.md +263 -0
  92. data/docs/customization.md +595 -0
  93. data/docs/hotwire.md +94 -0
  94. data/docs/initialization_prompt.md +36 -0
  95. data/docs/migration_1_to_2.md +318 -0
  96. data/docs/new_app_strategy.md +22 -0
  97. data/docs/patterns/application_foundation.md +136 -0
  98. data/docs/patterns/crud_resource.md +144 -0
  99. data/docs/patterns/destructive_action.md +105 -0
  100. data/docs/patterns/flash_and_toast.md +57 -0
  101. data/docs/patterns/inline_edit.md +59 -0
  102. data/docs/patterns/queryable_collection.md +187 -0
  103. data/docs/patterns/resource_form.md +126 -0
  104. data/docs/rails_conventions.md +95 -0
  105. data/docs/rails_integration.md +514 -0
  106. data/lib/generators/nitro_kit/install_generator.rb +48 -0
  107. data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +67 -0
  108. data/lib/nitro_kit/engine.rb +20 -0
  109. data/lib/nitro_kit/installation.rb +671 -0
  110. data/lib/nitro_kit/migration_inventory.rb +345 -0
  111. data/lib/nitro_kit/upgrade_smoke_test.rb +403 -0
  112. data/lib/nitro_kit/version.rb +1 -1
  113. data/lib/nitro_kit.rb +5 -42
  114. data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +42 -0
  115. data/lib/tasks/nitro_kit_tasks.rake +79 -4
  116. data/plugins/nitro-kit/.codex-plugin/plugin.json +28 -0
  117. data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +67 -0
  118. data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +4 -0
  119. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +60 -0
  120. data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +4 -0
  121. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +62 -0
  122. data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +4 -0
  123. data/src/stylesheets/nitro_kit/components/accordion.css +119 -0
  124. data/src/stylesheets/nitro_kit/components/alert.css +82 -0
  125. data/src/stylesheets/nitro_kit/components/app_navigation.css +156 -0
  126. data/src/stylesheets/nitro_kit/components/app_shell.css +605 -0
  127. data/src/stylesheets/nitro_kit/components/appearance_picker.css +117 -0
  128. data/src/stylesheets/nitro_kit/components/auth_shell.css +8 -0
  129. data/src/stylesheets/nitro_kit/components/avatar.css +81 -0
  130. data/src/stylesheets/nitro_kit/components/avatar_stack.css +80 -0
  131. data/src/stylesheets/nitro_kit/components/badge.css +76 -0
  132. data/src/stylesheets/nitro_kit/components/button.css +283 -0
  133. data/src/stylesheets/nitro_kit/components/button_group.css +53 -0
  134. data/src/stylesheets/nitro_kit/components/button_to.css +6 -0
  135. data/src/stylesheets/nitro_kit/components/card.css +70 -0
  136. data/src/stylesheets/nitro_kit/components/checkbox.css +160 -0
  137. data/src/stylesheets/nitro_kit/components/checkbox_group.css +61 -0
  138. data/src/stylesheets/nitro_kit/components/combobox.css +148 -0
  139. data/src/stylesheets/nitro_kit/components/command_palette.css +255 -0
  140. data/src/stylesheets/nitro_kit/components/container.css +25 -0
  141. data/src/stylesheets/nitro_kit/components/control_group.css +149 -0
  142. data/src/stylesheets/nitro_kit/components/danger_zone.css +54 -0
  143. data/src/stylesheets/nitro_kit/components/data_section.css +40 -0
  144. data/src/stylesheets/nitro_kit/components/details_table.css +53 -0
  145. data/src/stylesheets/nitro_kit/components/dialog.css +80 -0
  146. data/src/stylesheets/nitro_kit/components/dropdown.css +152 -0
  147. data/src/stylesheets/nitro_kit/components/dropzone.css +198 -0
  148. data/src/stylesheets/nitro_kit/components/empty_state.css +53 -0
  149. data/src/stylesheets/nitro_kit/components/field.css +79 -0
  150. data/src/stylesheets/nitro_kit/components/field_group.css +6 -0
  151. data/src/stylesheets/nitro_kit/components/fieldset.css +28 -0
  152. data/src/stylesheets/nitro_kit/components/flex.css +455 -0
  153. data/src/stylesheets/nitro_kit/components/form_section.css +71 -0
  154. data/src/stylesheets/nitro_kit/components/grid.css +308 -0
  155. data/src/stylesheets/nitro_kit/components/icon.css +26 -0
  156. data/src/stylesheets/nitro_kit/components/input.css +80 -0
  157. data/src/stylesheets/nitro_kit/components/label.css +9 -0
  158. data/src/stylesheets/nitro_kit/components/layout.css +440 -0
  159. data/src/stylesheets/nitro_kit/components/page_header.css +61 -0
  160. data/src/stylesheets/nitro_kit/components/pagination.css +119 -0
  161. data/src/stylesheets/nitro_kit/components/pagination_bar.css +41 -0
  162. data/src/stylesheets/nitro_kit/components/palette.css +113 -0
  163. data/src/stylesheets/nitro_kit/components/progressive_image.css +139 -0
  164. data/src/stylesheets/nitro_kit/components/radio_button.css +127 -0
  165. data/src/stylesheets/nitro_kit/components/radio_button_group.css +143 -0
  166. data/src/stylesheets/nitro_kit/components/rich_text_area.css +21 -0
  167. data/src/stylesheets/nitro_kit/components/select.css +70 -0
  168. data/src/stylesheets/nitro_kit/components/settings_layout.css +100 -0
  169. data/src/stylesheets/nitro_kit/components/sheet.css +100 -0
  170. data/src/stylesheets/nitro_kit/components/stat_grid.css +51 -0
  171. data/src/stylesheets/nitro_kit/components/switch.css +152 -0
  172. data/src/stylesheets/nitro_kit/components/table.css +116 -0
  173. data/src/stylesheets/nitro_kit/components/tabs.css +138 -0
  174. data/src/stylesheets/nitro_kit/components/textarea.css +39 -0
  175. data/src/stylesheets/nitro_kit/components/toast.css +131 -0
  176. data/src/stylesheets/nitro_kit/components/toolbar.css +78 -0
  177. data/src/stylesheets/nitro_kit/components/tooltip.css +124 -0
  178. data/src/stylesheets/nitro_kit/components/typeset.css +191 -0
  179. data/src/stylesheets/nitro_kit/layers.css +7 -0
  180. data/src/stylesheets/nitro_kit/reset.css +192 -0
  181. data/src/stylesheets/nitro_kit/tokens.css +198 -0
  182. metadata +182 -51
  183. data/Rakefile +0 -8
  184. data/app/assets/tailwind/application.css +0 -49
  185. data/app/components/nitro_kit/datepicker.rb +0 -9
  186. data/app/helpers/nitro_kit/accordion_helper.rb +0 -9
  187. data/app/helpers/nitro_kit/alert_helper.rb +0 -11
  188. data/app/helpers/nitro_kit/avatar_helper.rb +0 -13
  189. data/app/helpers/nitro_kit/badge_helper.rb +0 -13
  190. data/app/helpers/nitro_kit/button_group_helper.rb +0 -9
  191. data/app/helpers/nitro_kit/button_helper.rb +0 -49
  192. data/app/helpers/nitro_kit/card_helper.rb +0 -9
  193. data/app/helpers/nitro_kit/checkbox_helper.rb +0 -50
  194. data/app/helpers/nitro_kit/combobox_helper.rb +0 -9
  195. data/app/helpers/nitro_kit/datepicker_helper.rb +0 -9
  196. data/app/helpers/nitro_kit/dialog_helper.rb +0 -9
  197. data/app/helpers/nitro_kit/dropdown_helper.rb +0 -9
  198. data/app/helpers/nitro_kit/field_group_helper.rb +0 -9
  199. data/app/helpers/nitro_kit/field_helper.rb +0 -9
  200. data/app/helpers/nitro_kit/fieldset_helper.rb +0 -9
  201. data/app/helpers/nitro_kit/form_helper.rb +0 -13
  202. data/app/helpers/nitro_kit/icon_helper.rb +0 -9
  203. data/app/helpers/nitro_kit/input_helper.rb +0 -37
  204. data/app/helpers/nitro_kit/label_helper.rb +0 -19
  205. data/app/helpers/nitro_kit/pagination_helper.rb +0 -46
  206. data/app/helpers/nitro_kit/radio_button_helper.rb +0 -23
  207. data/app/helpers/nitro_kit/select_helper.rb +0 -24
  208. data/app/helpers/nitro_kit/switch_helper.rb +0 -9
  209. data/app/helpers/nitro_kit/table_helper.rb +0 -9
  210. data/app/helpers/nitro_kit/tabs_helper.rb +0 -9
  211. data/app/helpers/nitro_kit/textarea_helper.rb +0 -9
  212. data/app/helpers/nitro_kit/toast_helper.rb +0 -25
  213. data/app/helpers/nitro_kit/tooltip_helper.rb +0 -9
  214. data/app/javascript/controllers/nk/accordion_controller.js +0 -18
  215. data/app/javascript/controllers/nk/datepicker_controller.js +0 -5
  216. data/app/javascript/controllers/nk/switch_controller.js +0 -32
  217. data/lib/generators/nitro_kit/component_generator.rb +0 -84
  218. data/lib/nitro_kit/schema_builder.rb +0 -129
  219. data/lib/nitro_kit/variants.rb +0 -21
@@ -2,45 +2,157 @@ import { Controller } from "@hotwired/stimulus";
2
2
 
3
3
  export default class extends Controller {
4
4
  static targets = ["tab", "panel"];
5
- static values = { active: String };
5
+ static values = {
6
+ active: String,
7
+ activation: { type: String, default: "automatic" },
8
+ orientation: { type: String, default: "horizontal" },
9
+ };
6
10
 
7
- setActiveTab(event) {
8
- this.activeValue = event.params.key;
11
+ connect() {
12
+ this.enhanced = true;
13
+ this.element.dataset.enhanced = "true";
14
+ this.synchronize();
9
15
  }
10
16
 
11
- prevTab(event) {
12
- const prevTab = event.target.previousElementSibling;
13
- if (prevTab) {
14
- prevTab.click();
15
- prevTab.focus();
17
+ disconnect() {
18
+ this.enhanced = false;
19
+ this.reconcileScheduled = false;
20
+ delete this.element.dataset.enhanced;
21
+
22
+ this.element
23
+ .querySelectorAll(':scope > [data-slot="tabs-panel"]')
24
+ .forEach((panel) => {
25
+ panel.hidden = false;
26
+ panel.removeAttribute("aria-hidden");
27
+ panel.removeAttribute("tabindex");
28
+ });
29
+
30
+ this.element
31
+ .querySelectorAll(
32
+ ':scope > [data-slot="tabs-list"] > [data-slot="tabs-tab"]',
33
+ )
34
+ .forEach((tab) => {
35
+ tab.tabIndex = tab.disabled ? -1 : 0;
36
+ });
37
+ }
38
+
39
+ select(event) {
40
+ if (!event.currentTarget.disabled) {
41
+ this.activeValue = event.currentTarget.dataset.key;
16
42
  }
17
43
  }
18
44
 
19
- nextTab(event) {
20
- const nextTab = event.target.nextElementSibling;
21
- if (nextTab) {
22
- nextTab.click();
23
- nextTab.focus();
45
+ navigate(event) {
46
+ const directionKeys =
47
+ this.orientationValue === "vertical"
48
+ ? ["ArrowUp", "ArrowDown"]
49
+ : ["ArrowLeft", "ArrowRight"];
50
+
51
+ if (![...directionKeys, "Home", "End"].includes(event.key)) return;
52
+
53
+ event.preventDefault();
54
+
55
+ const enabledTabs = this.tabTargets.filter((tab) => !tab.disabled);
56
+ const currentIndex = enabledTabs.indexOf(event.currentTarget);
57
+
58
+ if (currentIndex === -1) return;
59
+
60
+ let nextIndex;
61
+
62
+ if (event.key === "Home") {
63
+ nextIndex = 0;
64
+ } else if (event.key === "End") {
65
+ nextIndex = enabledTabs.length - 1;
66
+ } else {
67
+ const offset = event.key === directionKeys[0] ? -1 : 1;
68
+ nextIndex =
69
+ (currentIndex + offset + enabledTabs.length) % enabledTabs.length;
24
70
  }
71
+
72
+ const nextTab = enabledTabs[nextIndex];
73
+
74
+ if (this.activationValue === "automatic") {
75
+ this.activeValue = nextTab.dataset.key;
76
+ } else {
77
+ this.tabTargets.forEach((tab) => {
78
+ tab.tabIndex = -1;
79
+ });
80
+ nextTab.tabIndex = 0;
81
+ }
82
+
83
+ nextTab.focus();
25
84
  }
26
85
 
27
86
  activeValueChanged() {
87
+ if (!this.enhanced) return;
88
+
89
+ this.synchronize();
90
+ }
91
+
92
+ panelTargetConnected() {
93
+ if (this.enhanced) this.synchronize();
94
+ }
95
+
96
+ panelTargetDisconnected() {
97
+ this.scheduleReconciliation();
98
+ }
99
+
100
+ tabTargetConnected() {
101
+ if (this.enhanced) this.synchronize();
102
+ }
103
+
104
+ tabTargetDisconnected() {
105
+ this.scheduleReconciliation();
106
+ }
107
+
108
+ scheduleReconciliation() {
109
+ if (!this.enhanced || this.reconcileScheduled) return;
110
+
111
+ this.reconcileScheduled = true;
112
+ queueMicrotask(() => {
113
+ this.reconcileScheduled = false;
114
+ if (this.enhanced) this.reconcileTargets();
115
+ });
116
+ }
117
+
118
+ reconcileTargets() {
119
+ const active = this.activeValue;
120
+ const hasActivePair =
121
+ this.tabTargets.some((tab) => tab.dataset.key === active) &&
122
+ this.panelTargets.some((panel) => panel.dataset.key === active);
123
+
124
+ if (!hasActivePair) {
125
+ const fallback = this.tabTargets.find(
126
+ (tab) =>
127
+ !tab.disabled &&
128
+ this.panelTargets.some(
129
+ (panel) => panel.dataset.key === tab.dataset.key,
130
+ ),
131
+ );
132
+ if (fallback) this.activeValue = fallback.dataset.key;
133
+ }
134
+
135
+ this.synchronize();
136
+ }
137
+
138
+ synchronize() {
28
139
  const value = this.activeValue;
29
140
 
30
141
  this.panelTargets.forEach((panel) => {
31
- if (panel.dataset.key === value) {
32
- panel.ariaHidden = false;
33
- } else {
34
- panel.ariaHidden = true;
35
- }
142
+ const active = panel.dataset.key === value;
143
+
144
+ panel.hidden = !active;
145
+ panel.setAttribute("aria-hidden", String(!active));
146
+ panel.dataset.state = active ? "active" : "inactive";
147
+ panel.tabIndex = active ? 0 : -1;
36
148
  });
37
149
 
38
150
  this.tabTargets.forEach((tab) => {
39
- if (tab.dataset.key === value) {
40
- tab.ariaSelected = true;
41
- } else {
42
- tab.ariaSelected = false;
43
- }
151
+ const active = tab.dataset.key === value;
152
+
153
+ tab.setAttribute("aria-selected", String(active));
154
+ tab.dataset.state = active ? "active" : "inactive";
155
+ tab.tabIndex = active && !tab.disabled ? 0 : -1;
44
156
  });
45
157
  }
46
158
  }
@@ -1,70 +1,137 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ["list", "template", "sink"];
5
- static values = {
6
- duration: { type: Number, default: 5000 },
7
- };
8
-
9
- connect() {
10
- if (this.hasSinkTarget) {
11
- this.mutationObserver = new MutationObserver(([event]) => {
12
- if (event.addedNodes.length === 0) return;
13
- this.#flushSink();
14
- });
15
- this.mutationObserver.observe(this.sinkTarget, { childList: true });
16
- }
17
-
18
- this.#flushSink();
19
- }
4
+ static targets = ["item"];
5
+ static values = { duration: Number };
20
6
 
21
7
  disconnect() {
22
- if (this.mutationObserver) this.mutationObserver.disconnect();
8
+ this.teardown();
9
+ }
10
+
11
+ teardown() {
12
+ this.timers?.forEach((timers) => {
13
+ window.clearTimeout(timers.dismiss);
14
+ window.clearTimeout(timers.remove);
15
+ });
16
+ this.timers?.clear();
17
+
18
+ this.itemTargets
19
+ .filter((item) => item.dataset.state === "closed")
20
+ .forEach((item) => item.remove());
23
21
  }
24
22
 
25
- toast({ params }) {
26
- const { title, description } = params;
27
- const item = this.templateTarget.content.cloneNode(true);
23
+ itemTargetConnected(item) {
24
+ this.schedule(item);
25
+ }
26
+
27
+ itemTargetDisconnected(item) {
28
+ this.clear(item);
29
+ }
30
+
31
+ dismiss(event) {
32
+ const item = event.currentTarget.closest('[data-nk="toast-item"]');
33
+ if (item) this.close(item);
34
+ }
28
35
 
29
- item.querySelector("[data-slot=title]").textContent = title;
30
- item.querySelector("[data-slot=description]").textContent = description;
36
+ pause(event) {
37
+ const item = event.currentTarget;
38
+ const timers = this.timerMap.get(item);
39
+ if (!timers?.dismiss) return;
31
40
 
32
- this.show(item);
41
+ window.clearTimeout(timers.dismiss);
42
+ timers.dismiss = null;
43
+ timers.remaining = Math.max(0, timers.deadline - Date.now());
33
44
  }
34
45
 
35
- show(item) {
36
- this.clear();
37
- this.listTarget.appendChild(item);
46
+ resume(event) {
47
+ const item = event.currentTarget;
48
+ if (item.contains(event.relatedTarget)) return;
49
+ if (item.matches(":hover, :focus-within")) return;
50
+ if (item.dataset.state !== "open") return;
38
51
 
39
- requestAnimationFrame(() => {
40
- this.listTarget.children[0].dataset.state = "open";
52
+ const remaining = this.timerMap.get(item)?.remaining ?? this.durationValue;
53
+ this.schedule(item, remaining);
54
+ }
55
+
56
+ remove(event) {
57
+ if (event.target !== event.currentTarget) return;
58
+ if (event.currentTarget.dataset.state !== "closed") return;
59
+
60
+ this.removeItem(event.currentTarget);
61
+ }
62
+
63
+ schedule(item, delay = this.durationValue) {
64
+ this.clear(item);
65
+ if (item.dataset.state !== "open") return;
66
+ if (item.hasAttribute("data-nk--toast-permanent")) return;
67
+
68
+ const dismiss = window.setTimeout(() => this.close(item), delay);
69
+ this.timerMap.set(item, {
70
+ dismiss,
71
+ remove: null,
72
+ deadline: Date.now() + delay,
73
+ remaining: delay,
41
74
  });
75
+ }
42
76
 
43
- if (this.timer) clearTimeout(this.timer);
77
+ close(item) {
78
+ if (!item.isConnected || item.dataset.state === "closed") return;
79
+
80
+ this.clear(item);
81
+ item.dataset.state = "closed";
82
+
83
+ const remove = window.setTimeout(
84
+ () => this.removeItem(item),
85
+ this.removalDelay(item),
86
+ );
87
+ this.timerMap.set(item, {
88
+ dismiss: null,
89
+ remove,
90
+ deadline: null,
91
+ remaining: null,
92
+ });
93
+ }
44
94
 
45
- this.timer = setTimeout(() => {
46
- this.hide();
47
- }, this.durationValue);
95
+ removeItem(item) {
96
+ this.clear(item);
97
+ item.remove();
48
98
  }
49
99
 
50
- hide() {
51
- this.listTarget.children[0].dataset.state = "closed";
100
+ clear(item) {
101
+ const timers = this.timerMap.get(item);
102
+ if (!timers) return;
52
103
 
53
- setTimeout(() => {
54
- this.clear();
55
- }, 250);
104
+ window.clearTimeout(timers.dismiss);
105
+ window.clearTimeout(timers.remove);
106
+ this.timerMap.delete(item);
56
107
  }
57
108
 
58
- clear() {
59
- this.listTarget.innerHTML = "";
109
+ removalDelay(item) {
110
+ const style = getComputedStyle(item);
111
+ const durations = style.transitionDuration
112
+ .split(",")
113
+ .map((value) => this.timeInMilliseconds(value));
114
+ const delays = style.transitionDelay
115
+ .split(",")
116
+ .map((value) => this.timeInMilliseconds(value));
117
+
118
+ return (
119
+ Math.max(
120
+ ...durations.map(
121
+ (duration, index) => duration + delays[index % delays.length],
122
+ ),
123
+ 0,
124
+ ) + 50
125
+ );
60
126
  }
61
127
 
62
- #flushSink() {
63
- if (!this.hasSinkTarget) return;
128
+ timeInMilliseconds(value) {
129
+ const time = Number.parseFloat(value) || 0;
130
+ return value.trim().endsWith("ms") ? time : time * 1000;
131
+ }
64
132
 
65
- for (const li of this.sinkTarget.children) {
66
- this.show(li.cloneNode(true));
67
- li.remove();
68
- }
133
+ get timerMap() {
134
+ this.timers ||= new Map();
135
+ return this.timers;
69
136
  }
70
137
  }
@@ -1,59 +1,24 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
- import {
3
- computePosition,
4
- offset,
5
- flip,
6
- shift,
7
- autoUpdate,
8
- } from "@floating-ui/dom";
9
2
 
10
3
  export default class extends Controller {
11
- static targets = ["content"];
12
- static values = {
13
- open: { type: Boolean, default: false },
14
- // Options for floating-ui
15
- placement: { type: String, default: "top" },
16
- };
17
-
18
- connect() {
19
- this.updatePosition();
20
- }
21
-
22
4
  disconnect() {
23
- this.close();
5
+ this.reset();
24
6
  }
25
7
 
26
- open() {
27
- this.openValue = true;
8
+ dismiss(event) {
9
+ event.preventDefault();
10
+ this.element.dataset.dismissed = "";
28
11
  }
29
12
 
30
- close() {
31
- this.openValue = false;
32
- }
33
-
34
- openValueChanged(state, _previous) {
35
- this.contentTarget.dataset.state = state ? "open" : "closed";
13
+ resetIfUninterested(event) {
14
+ if (event.relatedTarget && this.element.contains(event.relatedTarget))
15
+ return;
16
+ if (this.element.matches(":hover, :focus-within")) return;
36
17
 
37
- if (state) {
38
- this.clearAutoUpdate = autoUpdate(
39
- this.element,
40
- this.contentTarget,
41
- this.updatePosition,
42
- );
43
- } else {
44
- if (this.clearAutoUpdate) {
45
- this.clearAutoUpdate();
46
- }
47
- }
18
+ this.reset();
48
19
  }
49
20
 
50
- updatePosition = () => {
51
- computePosition(this.element, this.contentTarget, {
52
- placement: this.placementValue,
53
- middleware: [offset(5), flip(), shift({ padding: 5 })],
54
- }).then(({ x, y }) => {
55
- this.contentTarget.style.left = `${x}px`;
56
- this.contentTarget.style.top = `${y}px`;
57
- });
58
- };
21
+ reset() {
22
+ delete this.element.dataset.dismissed;
23
+ }
59
24
  }
@@ -0,0 +1,2 @@
1
+ pin "@rails/activestorage", to: "activestorage.esm.js"
2
+ pin_all_from File.expand_path("../app/javascript/controllers", __dir__), under: "controllers"
@@ -0,0 +1,98 @@
1
+ en:
2
+ nitro_kit:
3
+ app_shell:
4
+ skip_link: "Skip to content"
5
+ open_navigation: "Open navigation"
6
+ close_navigation: "Close navigation"
7
+ navigation_dialog: "Application navigation"
8
+ appearance_picker:
9
+ label: "Appearance"
10
+ preferences:
11
+ light: "Light"
12
+ dark: "Dark"
13
+ system: "System"
14
+ avatar_stack:
15
+ overflow: "%{count} more avatars"
16
+ combobox:
17
+ invalid_selection: "Choose an option."
18
+ no_results: "No options found."
19
+ results:
20
+ one: "%{count} option available."
21
+ other: "%{count} options available."
22
+ command_palette:
23
+ label: "Search"
24
+ placeholder: "Search destinations…"
25
+ empty: "No destinations found."
26
+ close: "Close search"
27
+ shortcut: "⌘K"
28
+ results:
29
+ one: "%{count} destination available."
30
+ other: "%{count} destinations available."
31
+ details_table:
32
+ empty: "Not provided"
33
+ boolean_true: "Yes"
34
+ boolean_false: "No"
35
+ dialog:
36
+ close: "Close dialog"
37
+ dropzone:
38
+ label: "Upload files"
39
+ prompt: "Drop files here or choose them from your device."
40
+ preview_list: "Selected files"
41
+ progress: "Upload progress"
42
+ progress_for: "Upload progress for %{name}"
43
+ remove: "Remove"
44
+ remove_file: "Remove %{name}"
45
+ queued: "Queued"
46
+ uploading: "Uploading"
47
+ uploading_percent: "Uploading %{percent}%"
48
+ uploaded: "Uploaded"
49
+ upload_failed: "Upload failed"
50
+ ready: "Ready to submit"
51
+ status:
52
+ disabled: "File upload is disabled."
53
+ empty: "No files selected."
54
+ selected:
55
+ one: "%{count} file selected."
56
+ other: "%{count} files selected."
57
+ uploading:
58
+ one: "Uploading %{count} file."
59
+ other: "Uploading %{count} files."
60
+ attention:
61
+ one: "%{count} file needs attention."
62
+ other: "%{count} files need attention."
63
+ uploaded:
64
+ one: "%{count} file uploaded."
65
+ other: "%{count} files uploaded."
66
+ ready:
67
+ one: "%{count} file ready to submit."
68
+ other: "%{count} files ready to submit."
69
+ errors:
70
+ upload_failed: "%{name} could not be uploaded."
71
+ upload_failed_detail: "%{name} could not be uploaded: %{error}"
72
+ uploads_in_progress: "Wait for uploads to finish before submitting the form."
73
+ failed_files: "Remove failed files or choose them again before submitting the form."
74
+ too_large: "%{name} is larger than %{size}."
75
+ not_accepted: "%{name} is not an accepted file type."
76
+ max_files:
77
+ one: "Choose no more than %{count} file."
78
+ other: "Choose no more than %{count} files."
79
+ field:
80
+ options_legend: "Options"
81
+ form:
82
+ submit: "Save changes"
83
+ pagination:
84
+ label: "Pagination"
85
+ previous: "Previous"
86
+ next: "Next"
87
+ previous_page: "Previous page"
88
+ next_page: "Next page"
89
+ ellipsis: "More pages"
90
+ progressive_image:
91
+ unavailable: "Image unavailable"
92
+ sheet:
93
+ close: "Close panel"
94
+ tabs:
95
+ label: "Tabs"
96
+ toast:
97
+ label: "Notifications"
98
+ dismiss: "Dismiss notification"
@@ -0,0 +1,139 @@
1
+ # Building applications with Nitro Kit
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.
4
+
5
+ ## Start from the installed version
6
+
7
+ From the application root, run:
8
+
9
+ ```sh
10
+ bundle show nitro_kit
11
+ ```
12
+
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
105
+
106
+ - 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:
130
+
131
+ ```sh
132
+ bin/rails generate nitro_kit:install
133
+ ```
134
+
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.