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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +229 -1
- data/README.md +25 -10
- data/STYLE_GUIDE.md +47 -5
- data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +3 -3
- data/app/assets/stylesheets/nitro_kit.css +2820 -1816
- data/app/components/nitro_kit/accordion.rb +2 -26
- data/app/components/nitro_kit/alert.rb +1 -11
- data/app/components/nitro_kit/app_navigation.rb +45 -11
- data/app/components/nitro_kit/app_shell.rb +1 -7
- data/app/components/nitro_kit/appearance_picker.rb +11 -12
- data/app/components/nitro_kit/avatar.rb +6 -1
- data/app/components/nitro_kit/avatar_stack.rb +3 -0
- data/app/components/nitro_kit/badge.rb +13 -4
- data/app/components/nitro_kit/button_group.rb +2 -2
- data/app/components/nitro_kit/checkbox.rb +7 -4
- data/app/components/nitro_kit/checkbox_group.rb +0 -25
- data/app/components/nitro_kit/combobox.rb +4 -33
- data/app/components/nitro_kit/command_palette.rb +42 -59
- data/app/components/nitro_kit/component.rb +57 -5
- data/app/components/nitro_kit/danger_zone.rb +5 -1
- data/app/components/nitro_kit/data_section.rb +28 -4
- data/app/components/nitro_kit/details_table.rb +19 -8
- data/app/components/nitro_kit/dialog.rb +23 -19
- data/app/components/nitro_kit/dropdown.rb +1 -6
- data/app/components/nitro_kit/dropzone.rb +4 -15
- data/app/components/nitro_kit/empty_state.rb +21 -2
- data/app/components/nitro_kit/field.rb +35 -11
- data/app/components/nitro_kit/form_builder.rb +1 -2
- data/app/components/nitro_kit/progressive_image.rb +2 -2
- data/app/components/nitro_kit/radio_button.rb +7 -4
- data/app/components/nitro_kit/radio_button_group.rb +0 -25
- data/app/components/nitro_kit/select.rb +4 -17
- data/app/components/nitro_kit/settings_layout.rb +17 -4
- data/app/components/nitro_kit/{form_section.rb → settings_section.rb} +18 -11
- data/app/components/nitro_kit/sheet.rb +29 -20
- data/app/components/nitro_kit/stat_grid.rb +18 -2
- data/app/components/nitro_kit/table.rb +22 -3
- data/app/components/nitro_kit/tabs.rb +2 -13
- data/app/components/nitro_kit/toast.rb +4 -2
- data/app/components/nitro_kit/toolbar.rb +2 -0
- data/app/components/nitro_kit/tooltip.rb +7 -10
- data/app/javascript/controllers/nk/app_shell_controller.js +3 -3
- data/app/javascript/controllers/nk/appearance_controller.js +6 -0
- data/app/javascript/controllers/nk/combobox_controller.js +51 -1
- data/app/javascript/controllers/nk/command_palette_controller.js +25 -11
- data/app/javascript/controllers/nk/dialog_controller.js +54 -0
- data/app/javascript/controllers/nk/dropdown_controller.js +45 -0
- data/app/javascript/controllers/nk/dropzone_controller.js +2 -2
- data/app/javascript/controllers/nk/progressive_image_controller.js +11 -7
- data/app/javascript/controllers/nk/tabs_controller.js +21 -2
- data/app/javascript/controllers/nk/tooltip_controller.js +9 -0
- data/config/locales/en.yml +4 -1
- data/docs/agent_guide.md +63 -123
- data/docs/agent_native_spec.md +72 -383
- data/docs/browser_support.md +82 -0
- data/docs/component_contracts.md +108 -82
- data/docs/customization.md +689 -91
- data/docs/hotwire.md +50 -76
- data/docs/initialization_prompt.md +26 -31
- data/docs/migration_1_to_2.md +88 -30
- data/docs/patterns/application_foundation.md +45 -119
- data/docs/patterns/crud_resource.md +44 -126
- data/docs/patterns/destructive_action.md +45 -77
- data/docs/patterns/flash_and_toast.md +18 -36
- data/docs/patterns/inline_edit.md +21 -44
- data/docs/patterns/queryable_collection.md +65 -161
- data/docs/patterns/resource_form.md +33 -81
- data/docs/rails_conventions.md +38 -75
- data/docs/rails_integration.md +96 -446
- data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +12 -0
- data/lib/nitro_kit/installation.rb +20 -7
- data/lib/nitro_kit/migration_inventory.rb +275 -8
- data/lib/nitro_kit/upgrade_smoke_test.rb +10 -12
- data/lib/nitro_kit/version.rb +1 -1
- data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +10 -3
- data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +4 -0
- data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +13 -1
- data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +32 -15
- data/src/stylesheets/nitro_kit/components/accordion.css +44 -44
- data/src/stylesheets/nitro_kit/components/alert.css +27 -36
- data/src/stylesheets/nitro_kit/components/app_navigation.css +92 -20
- data/src/stylesheets/nitro_kit/components/app_shell.css +199 -196
- data/src/stylesheets/nitro_kit/components/appearance_picker.css +61 -39
- data/src/stylesheets/nitro_kit/components/auth_shell.css +2 -2
- data/src/stylesheets/nitro_kit/components/avatar.css +15 -11
- data/src/stylesheets/nitro_kit/components/avatar_stack.css +48 -29
- data/src/stylesheets/nitro_kit/components/badge.css +13 -9
- data/src/stylesheets/nitro_kit/components/button.css +121 -79
- data/src/stylesheets/nitro_kit/components/button_group.css +19 -14
- data/src/stylesheets/nitro_kit/components/card.css +12 -6
- data/src/stylesheets/nitro_kit/components/checkbox.css +54 -67
- data/src/stylesheets/nitro_kit/components/checkbox_group.css +16 -16
- data/src/stylesheets/nitro_kit/components/combobox.css +39 -29
- data/src/stylesheets/nitro_kit/components/command_palette.css +110 -51
- data/src/stylesheets/nitro_kit/components/container.css +6 -6
- data/src/stylesheets/nitro_kit/components/control_group.css +61 -67
- data/src/stylesheets/nitro_kit/components/danger_zone.css +8 -8
- data/src/stylesheets/nitro_kit/components/data_section.css +4 -4
- data/src/stylesheets/nitro_kit/components/details_table.css +17 -15
- data/src/stylesheets/nitro_kit/components/dialog.css +8 -5
- data/src/stylesheets/nitro_kit/components/dropdown.css +36 -30
- data/src/stylesheets/nitro_kit/components/dropzone.css +78 -35
- data/src/stylesheets/nitro_kit/components/empty_state.css +7 -7
- data/src/stylesheets/nitro_kit/components/field.css +32 -28
- data/src/stylesheets/nitro_kit/components/field_group.css +13 -0
- data/src/stylesheets/nitro_kit/components/fieldset.css +8 -2
- data/src/stylesheets/nitro_kit/components/flex.css +1 -1
- data/src/stylesheets/nitro_kit/components/grid.css +1 -1
- data/src/stylesheets/nitro_kit/components/icon.css +5 -5
- data/src/stylesheets/nitro_kit/components/input.css +42 -8
- data/src/stylesheets/nitro_kit/components/layout.css +165 -165
- data/src/stylesheets/nitro_kit/components/page_header.css +7 -7
- data/src/stylesheets/nitro_kit/components/pagination.css +58 -49
- data/src/stylesheets/nitro_kit/components/pagination_bar.css +9 -9
- data/src/stylesheets/nitro_kit/components/palette.css +145 -77
- data/src/stylesheets/nitro_kit/components/progressive_image.css +45 -55
- data/src/stylesheets/nitro_kit/components/radio_button.css +48 -46
- data/src/stylesheets/nitro_kit/components/radio_button_group.css +49 -49
- data/src/stylesheets/nitro_kit/components/rich_text_area.css +16 -1
- data/src/stylesheets/nitro_kit/components/select.css +24 -15
- data/src/stylesheets/nitro_kit/components/settings_layout.css +26 -23
- data/src/stylesheets/nitro_kit/components/settings_section.css +84 -0
- data/src/stylesheets/nitro_kit/components/sheet.css +30 -10
- data/src/stylesheets/nitro_kit/components/stat_grid.css +11 -10
- data/src/stylesheets/nitro_kit/components/switch.css +60 -62
- data/src/stylesheets/nitro_kit/components/table.css +43 -35
- data/src/stylesheets/nitro_kit/components/tabs.css +48 -40
- data/src/stylesheets/nitro_kit/components/textarea.css +20 -5
- data/src/stylesheets/nitro_kit/components/toast.css +11 -47
- data/src/stylesheets/nitro_kit/components/toolbar.css +16 -41
- data/src/stylesheets/nitro_kit/components/tooltip.css +38 -29
- data/src/stylesheets/nitro_kit/components/typeset.css +150 -4
- data/src/stylesheets/nitro_kit/reset.css +15 -15
- data/src/stylesheets/nitro_kit/tokens.css +501 -79
- metadata +4 -4
- data/docs/new_app_strategy.md +0 -22
- 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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
openedByTrigger(event) {
|
|
59
|
+
if (!this.canOpen) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
62
64
|
|
|
63
|
-
this.returnFocus =
|
|
65
|
+
this.returnFocus = event.currentTarget;
|
|
64
66
|
this.restoreFocus = true;
|
|
65
|
-
this.
|
|
66
|
-
this.
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
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
|
}
|
|
@@ -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)
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
}
|
data/config/locales/en.yml
CHANGED
|
@@ -12,7 +12,9 @@ en:
|
|
|
12
12
|
dark: "Dark"
|
|
13
13
|
system: "System"
|
|
14
14
|
avatar_stack:
|
|
15
|
-
overflow:
|
|
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
|
-
#
|
|
1
|
+
# Nitro Kit application guide for coding agents
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
##
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
108
|
-
- Use
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
|
|
114
|
-
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
136
|
-
|
|
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.
|