nitro_kit 0.9.0 → 2.0.0.alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.agents/plugins/marketplace.json +20 -0
- data/CHANGELOG.md +172 -0
- data/LICENSE +28 -0
- data/README.md +22 -15
- data/STYLE_GUIDE.md +425 -0
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
- data/app/assets/stylesheets/nitro_kit.css +7239 -0
- data/app/components/nitro_kit/accordion.rb +125 -65
- data/app/components/nitro_kit/alert.rb +80 -43
- data/app/components/nitro_kit/app_navigation.rb +307 -0
- data/app/components/nitro_kit/app_shell.rb +227 -0
- data/app/components/nitro_kit/appearance_bootstrap.rb +146 -0
- data/app/components/nitro_kit/appearance_picker.rb +156 -0
- data/app/components/nitro_kit/auth_shell.rb +34 -0
- data/app/components/nitro_kit/avatar.rb +85 -29
- data/app/components/nitro_kit/avatar_stack.rb +155 -9
- data/app/components/nitro_kit/badge.rb +43 -81
- data/app/components/nitro_kit/button.rb +180 -103
- data/app/components/nitro_kit/button_group.rb +63 -11
- data/app/components/nitro_kit/button_to.rb +69 -0
- data/app/components/nitro_kit/card.rb +42 -45
- data/app/components/nitro_kit/checkbox.rb +115 -62
- data/app/components/nitro_kit/checkbox_group.rb +131 -22
- data/app/components/nitro_kit/choice.rb +55 -0
- data/app/components/nitro_kit/combobox.rb +292 -105
- data/app/components/nitro_kit/command_palette.rb +412 -0
- data/app/components/nitro_kit/component.rb +322 -46
- data/app/components/nitro_kit/container.rb +32 -0
- data/app/components/nitro_kit/control_group.rb +45 -0
- data/app/components/nitro_kit/danger_zone.rb +78 -0
- data/app/components/nitro_kit/data_section.rb +106 -0
- data/app/components/nitro_kit/details_table.rb +229 -0
- data/app/components/nitro_kit/dialog.rb +257 -83
- data/app/components/nitro_kit/dropdown.rb +335 -122
- data/app/components/nitro_kit/dropzone.rb +326 -0
- data/app/components/nitro_kit/empty_state.rb +99 -0
- data/app/components/nitro_kit/field.rb +461 -236
- data/app/components/nitro_kit/field_group.rb +15 -8
- data/app/components/nitro_kit/fieldset.rb +37 -39
- data/app/components/nitro_kit/flex.rb +49 -0
- data/app/components/nitro_kit/form_builder.rb +313 -72
- data/app/components/nitro_kit/form_section.rb +79 -0
- data/app/components/nitro_kit/grid.rb +34 -0
- data/app/components/nitro_kit/icon.rb +54 -25
- data/app/components/nitro_kit/input.rb +97 -10
- data/app/components/nitro_kit/label.rb +22 -6
- data/app/components/nitro_kit/layout_options.rb +7 -0
- data/app/components/nitro_kit/page_header.rb +76 -0
- data/app/components/nitro_kit/pagination.rb +355 -69
- data/app/components/nitro_kit/pagination_bar.rb +90 -0
- data/app/components/nitro_kit/progressive_image.rb +181 -0
- data/app/components/nitro_kit/radio_button.rb +77 -55
- data/app/components/nitro_kit/radio_button_group.rb +107 -36
- data/app/components/nitro_kit/responsive_value.rb +94 -0
- data/app/components/nitro_kit/rich_text_area.rb +31 -0
- data/app/components/nitro_kit/select.rb +129 -64
- data/app/components/nitro_kit/settings_layout.rb +152 -0
- data/app/components/nitro_kit/sheet.rb +202 -0
- data/app/components/nitro_kit/stat_grid.rb +75 -0
- data/app/components/nitro_kit/switch.rb +108 -51
- data/app/components/nitro_kit/table.rb +237 -45
- data/app/components/nitro_kit/tabs.rb +168 -63
- data/app/components/nitro_kit/textarea.rb +81 -10
- data/app/components/nitro_kit/toast.rb +211 -80
- data/app/components/nitro_kit/toolbar.rb +75 -0
- data/app/components/nitro_kit/tooltip.rb +235 -34
- data/app/components/nitro_kit/typeset.rb +28 -0
- data/app/javascript/controllers/nk/app_shell_controller.js +224 -0
- data/app/javascript/controllers/nk/appearance_controller.js +73 -0
- data/app/javascript/controllers/nk/avatar_controller.js +15 -0
- data/app/javascript/controllers/nk/button_controller.js +56 -0
- data/app/javascript/controllers/nk/checkable_controller.js +41 -0
- data/app/javascript/controllers/nk/combobox_controller.js +314 -82
- data/app/javascript/controllers/nk/command_palette_controller.js +249 -0
- data/app/javascript/controllers/nk/dialog_controller.js +14 -10
- data/app/javascript/controllers/nk/dropdown_controller.js +106 -58
- data/app/javascript/controllers/nk/dropzone/direct_upload.js +61 -0
- data/app/javascript/controllers/nk/dropzone/file_rules.js +77 -0
- data/app/javascript/controllers/nk/dropzone/form_submit_lock.js +30 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +482 -0
- data/app/javascript/controllers/nk/overlay_position.js +37 -0
- data/app/javascript/controllers/nk/progressive_image_controller.js +116 -0
- data/app/javascript/controllers/nk/tabs_controller.js +135 -23
- data/app/javascript/controllers/nk/toast_controller.js +112 -45
- data/app/javascript/controllers/nk/tooltip_controller.js +12 -47
- data/config/importmap.rb +2 -0
- data/config/locales/en.yml +98 -0
- data/docs/agent_guide.md +139 -0
- data/docs/agent_native_spec.md +422 -0
- data/docs/component_contracts.md +263 -0
- data/docs/customization.md +595 -0
- data/docs/hotwire.md +94 -0
- data/docs/initialization_prompt.md +36 -0
- data/docs/migration_1_to_2.md +318 -0
- data/docs/new_app_strategy.md +22 -0
- data/docs/patterns/application_foundation.md +136 -0
- data/docs/patterns/crud_resource.md +144 -0
- data/docs/patterns/destructive_action.md +105 -0
- data/docs/patterns/flash_and_toast.md +57 -0
- data/docs/patterns/inline_edit.md +59 -0
- data/docs/patterns/queryable_collection.md +187 -0
- data/docs/patterns/resource_form.md +126 -0
- data/docs/rails_conventions.md +95 -0
- data/docs/rails_integration.md +514 -0
- data/lib/generators/nitro_kit/install_generator.rb +48 -0
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +67 -0
- data/lib/nitro_kit/engine.rb +20 -0
- data/lib/nitro_kit/installation.rb +671 -0
- data/lib/nitro_kit/migration_inventory.rb +345 -0
- data/lib/nitro_kit/upgrade_smoke_test.rb +403 -0
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/nitro_kit.rb +5 -42
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +42 -0
- data/lib/tasks/nitro_kit_tasks.rake +79 -4
- data/plugins/nitro-kit/.codex-plugin/plugin.json +28 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +67 -0
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +60 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +62 -0
- data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +4 -0
- data/src/stylesheets/nitro_kit/components/accordion.css +119 -0
- data/src/stylesheets/nitro_kit/components/alert.css +82 -0
- data/src/stylesheets/nitro_kit/components/app_navigation.css +156 -0
- data/src/stylesheets/nitro_kit/components/app_shell.css +605 -0
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +117 -0
- data/src/stylesheets/nitro_kit/components/auth_shell.css +8 -0
- data/src/stylesheets/nitro_kit/components/avatar.css +81 -0
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +80 -0
- data/src/stylesheets/nitro_kit/components/badge.css +76 -0
- data/src/stylesheets/nitro_kit/components/button.css +283 -0
- data/src/stylesheets/nitro_kit/components/button_group.css +53 -0
- data/src/stylesheets/nitro_kit/components/button_to.css +6 -0
- data/src/stylesheets/nitro_kit/components/card.css +70 -0
- data/src/stylesheets/nitro_kit/components/checkbox.css +160 -0
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +61 -0
- data/src/stylesheets/nitro_kit/components/combobox.css +148 -0
- data/src/stylesheets/nitro_kit/components/command_palette.css +255 -0
- data/src/stylesheets/nitro_kit/components/container.css +25 -0
- data/src/stylesheets/nitro_kit/components/control_group.css +149 -0
- data/src/stylesheets/nitro_kit/components/danger_zone.css +54 -0
- data/src/stylesheets/nitro_kit/components/data_section.css +40 -0
- data/src/stylesheets/nitro_kit/components/details_table.css +53 -0
- data/src/stylesheets/nitro_kit/components/dialog.css +80 -0
- data/src/stylesheets/nitro_kit/components/dropdown.css +152 -0
- data/src/stylesheets/nitro_kit/components/dropzone.css +198 -0
- data/src/stylesheets/nitro_kit/components/empty_state.css +53 -0
- data/src/stylesheets/nitro_kit/components/field.css +79 -0
- data/src/stylesheets/nitro_kit/components/field_group.css +6 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +28 -0
- data/src/stylesheets/nitro_kit/components/flex.css +455 -0
- data/src/stylesheets/nitro_kit/components/form_section.css +71 -0
- data/src/stylesheets/nitro_kit/components/grid.css +308 -0
- data/src/stylesheets/nitro_kit/components/icon.css +26 -0
- data/src/stylesheets/nitro_kit/components/input.css +80 -0
- data/src/stylesheets/nitro_kit/components/label.css +9 -0
- data/src/stylesheets/nitro_kit/components/layout.css +440 -0
- data/src/stylesheets/nitro_kit/components/page_header.css +61 -0
- data/src/stylesheets/nitro_kit/components/pagination.css +119 -0
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +41 -0
- data/src/stylesheets/nitro_kit/components/palette.css +113 -0
- data/src/stylesheets/nitro_kit/components/progressive_image.css +139 -0
- data/src/stylesheets/nitro_kit/components/radio_button.css +127 -0
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +143 -0
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +21 -0
- data/src/stylesheets/nitro_kit/components/select.css +70 -0
- data/src/stylesheets/nitro_kit/components/settings_layout.css +100 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +100 -0
- data/src/stylesheets/nitro_kit/components/stat_grid.css +51 -0
- data/src/stylesheets/nitro_kit/components/switch.css +152 -0
- data/src/stylesheets/nitro_kit/components/table.css +116 -0
- data/src/stylesheets/nitro_kit/components/tabs.css +138 -0
- data/src/stylesheets/nitro_kit/components/textarea.css +39 -0
- data/src/stylesheets/nitro_kit/components/toast.css +131 -0
- data/src/stylesheets/nitro_kit/components/toolbar.css +78 -0
- data/src/stylesheets/nitro_kit/components/tooltip.css +124 -0
- data/src/stylesheets/nitro_kit/components/typeset.css +191 -0
- data/src/stylesheets/nitro_kit/layers.css +7 -0
- data/src/stylesheets/nitro_kit/reset.css +192 -0
- data/src/stylesheets/nitro_kit/tokens.css +198 -0
- metadata +182 -51
- data/Rakefile +0 -8
- data/app/assets/tailwind/application.css +0 -49
- data/app/components/nitro_kit/datepicker.rb +0 -9
- data/app/helpers/nitro_kit/accordion_helper.rb +0 -9
- data/app/helpers/nitro_kit/alert_helper.rb +0 -11
- data/app/helpers/nitro_kit/avatar_helper.rb +0 -13
- data/app/helpers/nitro_kit/badge_helper.rb +0 -13
- data/app/helpers/nitro_kit/button_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/button_helper.rb +0 -49
- data/app/helpers/nitro_kit/card_helper.rb +0 -9
- data/app/helpers/nitro_kit/checkbox_helper.rb +0 -50
- data/app/helpers/nitro_kit/combobox_helper.rb +0 -9
- data/app/helpers/nitro_kit/datepicker_helper.rb +0 -9
- data/app/helpers/nitro_kit/dialog_helper.rb +0 -9
- data/app/helpers/nitro_kit/dropdown_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_group_helper.rb +0 -9
- data/app/helpers/nitro_kit/field_helper.rb +0 -9
- data/app/helpers/nitro_kit/fieldset_helper.rb +0 -9
- data/app/helpers/nitro_kit/form_helper.rb +0 -13
- data/app/helpers/nitro_kit/icon_helper.rb +0 -9
- data/app/helpers/nitro_kit/input_helper.rb +0 -37
- data/app/helpers/nitro_kit/label_helper.rb +0 -19
- data/app/helpers/nitro_kit/pagination_helper.rb +0 -46
- data/app/helpers/nitro_kit/radio_button_helper.rb +0 -23
- data/app/helpers/nitro_kit/select_helper.rb +0 -24
- data/app/helpers/nitro_kit/switch_helper.rb +0 -9
- data/app/helpers/nitro_kit/table_helper.rb +0 -9
- data/app/helpers/nitro_kit/tabs_helper.rb +0 -9
- data/app/helpers/nitro_kit/textarea_helper.rb +0 -9
- data/app/helpers/nitro_kit/toast_helper.rb +0 -25
- data/app/helpers/nitro_kit/tooltip_helper.rb +0 -9
- data/app/javascript/controllers/nk/accordion_controller.js +0 -18
- data/app/javascript/controllers/nk/datepicker_controller.js +0 -5
- data/app/javascript/controllers/nk/switch_controller.js +0 -32
- data/lib/generators/nitro_kit/component_generator.rb +0 -84
- data/lib/nitro_kit/schema_builder.rb +0 -129
- data/lib/nitro_kit/variants.rb +0 -21
|
@@ -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 = {
|
|
5
|
+
static values = {
|
|
6
|
+
active: String,
|
|
7
|
+
activation: { type: String, default: "automatic" },
|
|
8
|
+
orientation: { type: String, default: "horizontal" },
|
|
9
|
+
};
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
this.
|
|
11
|
+
connect() {
|
|
12
|
+
this.enhanced = true;
|
|
13
|
+
this.element.dataset.enhanced = "true";
|
|
14
|
+
this.synchronize();
|
|
9
15
|
}
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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 = ["
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
item
|
|
36
|
+
pause(event) {
|
|
37
|
+
const item = event.currentTarget;
|
|
38
|
+
const timers = this.timerMap.get(item);
|
|
39
|
+
if (!timers?.dismiss) return;
|
|
31
40
|
|
|
32
|
-
|
|
41
|
+
window.clearTimeout(timers.dismiss);
|
|
42
|
+
timers.dismiss = null;
|
|
43
|
+
timers.remaining = Math.max(0, timers.deadline - Date.now());
|
|
33
44
|
}
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
95
|
+
removeItem(item) {
|
|
96
|
+
this.clear(item);
|
|
97
|
+
item.remove();
|
|
48
98
|
}
|
|
49
99
|
|
|
50
|
-
|
|
51
|
-
this.
|
|
100
|
+
clear(item) {
|
|
101
|
+
const timers = this.timerMap.get(item);
|
|
102
|
+
if (!timers) return;
|
|
52
103
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
104
|
+
window.clearTimeout(timers.dismiss);
|
|
105
|
+
window.clearTimeout(timers.remove);
|
|
106
|
+
this.timerMap.delete(item);
|
|
56
107
|
}
|
|
57
108
|
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
128
|
+
timeInMilliseconds(value) {
|
|
129
|
+
const time = Number.parseFloat(value) || 0;
|
|
130
|
+
return value.trim().endsWith("ms") ? time : time * 1000;
|
|
131
|
+
}
|
|
64
132
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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.
|
|
5
|
+
this.reset();
|
|
24
6
|
}
|
|
25
7
|
|
|
26
|
-
|
|
27
|
-
|
|
8
|
+
dismiss(event) {
|
|
9
|
+
event.preventDefault();
|
|
10
|
+
this.element.dataset.dismissed = "";
|
|
28
11
|
}
|
|
29
12
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
}
|
data/config/importmap.rb
ADDED
|
@@ -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"
|
data/docs/agent_guide.md
ADDED
|
@@ -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.
|