nitro_kit 2.0.0.alpha.2 → 2.0.0.alpha.3

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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/README.md +23 -4
  4. data/STYLE_GUIDE.md +37 -5
  5. data/app/assets/stylesheets/nitro_kit.css +1714 -1358
  6. data/app/components/nitro_kit/accordion.rb +1 -7
  7. data/app/components/nitro_kit/app_navigation.rb +1 -2
  8. data/app/components/nitro_kit/app_shell.rb +1 -7
  9. data/app/components/nitro_kit/appearance_picker.rb +1 -11
  10. data/app/components/nitro_kit/avatar.rb +6 -1
  11. data/app/components/nitro_kit/avatar_stack.rb +3 -0
  12. data/app/components/nitro_kit/button_group.rb +2 -2
  13. data/app/components/nitro_kit/checkbox.rb +7 -4
  14. data/app/components/nitro_kit/checkbox_group.rb +0 -25
  15. data/app/components/nitro_kit/combobox.rb +3 -16
  16. data/app/components/nitro_kit/command_palette.rb +41 -59
  17. data/app/components/nitro_kit/component.rb +57 -5
  18. data/app/components/nitro_kit/danger_zone.rb +5 -1
  19. data/app/components/nitro_kit/data_section.rb +28 -4
  20. data/app/components/nitro_kit/details_table.rb +19 -8
  21. data/app/components/nitro_kit/dialog.rb +23 -19
  22. data/app/components/nitro_kit/dropdown.rb +1 -6
  23. data/app/components/nitro_kit/dropzone.rb +3 -14
  24. data/app/components/nitro_kit/empty_state.rb +21 -2
  25. data/app/components/nitro_kit/field.rb +35 -11
  26. data/app/components/nitro_kit/form_builder.rb +0 -1
  27. data/app/components/nitro_kit/progressive_image.rb +2 -2
  28. data/app/components/nitro_kit/radio_button.rb +7 -4
  29. data/app/components/nitro_kit/radio_button_group.rb +0 -25
  30. data/app/components/nitro_kit/select.rb +3 -3
  31. data/app/components/nitro_kit/settings_layout.rb +17 -4
  32. data/app/components/nitro_kit/{form_section.rb → settings_section.rb} +18 -11
  33. data/app/components/nitro_kit/sheet.rb +29 -20
  34. data/app/components/nitro_kit/stat_grid.rb +18 -2
  35. data/app/components/nitro_kit/table.rb +22 -3
  36. data/app/components/nitro_kit/tabs.rb +1 -13
  37. data/app/components/nitro_kit/toast.rb +4 -2
  38. data/app/components/nitro_kit/toolbar.rb +2 -0
  39. data/app/components/nitro_kit/tooltip.rb +7 -10
  40. data/app/javascript/controllers/nk/app_shell_controller.js +3 -3
  41. data/app/javascript/controllers/nk/appearance_controller.js +6 -0
  42. data/app/javascript/controllers/nk/combobox_controller.js +51 -1
  43. data/app/javascript/controllers/nk/command_palette_controller.js +21 -11
  44. data/app/javascript/controllers/nk/dialog_controller.js +54 -0
  45. data/app/javascript/controllers/nk/dropdown_controller.js +45 -0
  46. data/app/javascript/controllers/nk/dropzone_controller.js +2 -2
  47. data/app/javascript/controllers/nk/progressive_image_controller.js +11 -7
  48. data/app/javascript/controllers/nk/tabs_controller.js +8 -1
  49. data/app/javascript/controllers/nk/tooltip_controller.js +9 -0
  50. data/config/locales/en.yml +4 -1
  51. data/docs/agent_guide.md +31 -16
  52. data/docs/agent_native_spec.md +27 -10
  53. data/docs/browser_support.md +193 -0
  54. data/docs/component_contracts.md +88 -82
  55. data/docs/customization.md +27 -15
  56. data/docs/hotwire.md +15 -3
  57. data/docs/initialization_prompt.md +31 -6
  58. data/docs/migration_1_to_2.md +61 -19
  59. data/docs/patterns/application_foundation.md +6 -4
  60. data/docs/patterns/crud_resource.md +6 -3
  61. data/docs/patterns/destructive_action.md +25 -0
  62. data/docs/patterns/resource_form.md +1 -1
  63. data/docs/rails_conventions.md +4 -1
  64. data/docs/rails_integration.md +39 -11
  65. data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +12 -0
  66. data/lib/nitro_kit/installation.rb +9 -4
  67. data/lib/nitro_kit/migration_inventory.rb +268 -8
  68. data/lib/nitro_kit/upgrade_smoke_test.rb +10 -12
  69. data/lib/nitro_kit/version.rb +1 -1
  70. data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +10 -3
  71. data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +4 -0
  72. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +6 -1
  73. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +18 -8
  74. data/src/stylesheets/nitro_kit/components/accordion.css +40 -40
  75. data/src/stylesheets/nitro_kit/components/alert.css +9 -9
  76. data/src/stylesheets/nitro_kit/components/app_navigation.css +19 -19
  77. data/src/stylesheets/nitro_kit/components/app_shell.css +196 -193
  78. data/src/stylesheets/nitro_kit/components/appearance_picker.css +41 -31
  79. data/src/stylesheets/nitro_kit/components/avatar.css +3 -3
  80. data/src/stylesheets/nitro_kit/components/avatar_stack.css +36 -19
  81. data/src/stylesheets/nitro_kit/components/button.css +83 -58
  82. data/src/stylesheets/nitro_kit/components/button_group.css +19 -14
  83. data/src/stylesheets/nitro_kit/components/card.css +11 -5
  84. data/src/stylesheets/nitro_kit/components/checkbox.css +40 -36
  85. data/src/stylesheets/nitro_kit/components/checkbox_group.css +13 -13
  86. data/src/stylesheets/nitro_kit/components/combobox.css +33 -23
  87. data/src/stylesheets/nitro_kit/components/command_palette.css +48 -47
  88. data/src/stylesheets/nitro_kit/components/control_group.css +52 -58
  89. data/src/stylesheets/nitro_kit/components/danger_zone.css +3 -3
  90. data/src/stylesheets/nitro_kit/components/data_section.css +1 -1
  91. data/src/stylesheets/nitro_kit/components/details_table.css +13 -13
  92. data/src/stylesheets/nitro_kit/components/dialog.css +6 -3
  93. data/src/stylesheets/nitro_kit/components/dropdown.css +29 -23
  94. data/src/stylesheets/nitro_kit/components/dropzone.css +71 -28
  95. data/src/stylesheets/nitro_kit/components/empty_state.css +6 -6
  96. data/src/stylesheets/nitro_kit/components/field.css +30 -26
  97. data/src/stylesheets/nitro_kit/components/field_group.css +13 -0
  98. data/src/stylesheets/nitro_kit/components/fieldset.css +6 -0
  99. data/src/stylesheets/nitro_kit/components/input.css +9 -2
  100. data/src/stylesheets/nitro_kit/components/layout.css +165 -165
  101. data/src/stylesheets/nitro_kit/components/page_header.css +3 -3
  102. data/src/stylesheets/nitro_kit/components/pagination.css +55 -46
  103. data/src/stylesheets/nitro_kit/components/pagination_bar.css +7 -7
  104. data/src/stylesheets/nitro_kit/components/palette.css +20 -20
  105. data/src/stylesheets/nitro_kit/components/progressive_image.css +44 -54
  106. data/src/stylesheets/nitro_kit/components/radio_button.css +36 -29
  107. data/src/stylesheets/nitro_kit/components/radio_button_group.css +43 -43
  108. data/src/stylesheets/nitro_kit/components/rich_text_area.css +15 -0
  109. data/src/stylesheets/nitro_kit/components/select.css +2 -9
  110. data/src/stylesheets/nitro_kit/components/settings_layout.css +21 -18
  111. data/src/stylesheets/nitro_kit/components/settings_section.css +84 -0
  112. data/src/stylesheets/nitro_kit/components/sheet.css +23 -3
  113. data/src/stylesheets/nitro_kit/components/stat_grid.css +10 -9
  114. data/src/stylesheets/nitro_kit/components/switch.css +37 -33
  115. data/src/stylesheets/nitro_kit/components/table.css +35 -31
  116. data/src/stylesheets/nitro_kit/components/tabs.css +43 -39
  117. data/src/stylesheets/nitro_kit/components/toast.css +20 -9
  118. data/src/stylesheets/nitro_kit/components/toolbar.css +14 -39
  119. data/src/stylesheets/nitro_kit/components/tooltip.css +34 -29
  120. data/src/stylesheets/nitro_kit/components/typeset.css +147 -1
  121. data/src/stylesheets/nitro_kit/reset.css +9 -9
  122. data/src/stylesheets/nitro_kit/tokens.css +8 -9
  123. metadata +4 -3
  124. data/src/stylesheets/nitro_kit/components/form_section.css +0 -71
@@ -7,8 +7,13 @@ import {
7
7
  export default class extends Controller {
8
8
  static targets = ["trigger", "content", "item"];
9
9
 
10
+ connect() {
11
+ this.outsidePointerDown = this.outsidePointerDown.bind(this);
12
+ }
13
+
10
14
  disconnect() {
11
15
  this.stopPositioning?.();
16
+ this.stopOutsidePointerFallback();
12
17
  }
13
18
 
14
19
  openFromKeyboard(event) {
@@ -36,10 +41,12 @@ export default class extends Controller {
36
41
  focusOpened(event) {
37
42
  if (event.newState === "open") {
38
43
  this.startPositioning();
44
+ this.startOutsidePointerFallback();
39
45
  this.focusInitialItem();
40
46
  } else {
41
47
  this.stopPositioning?.();
42
48
  this.stopPositioning = null;
49
+ this.stopOutsidePointerFallback();
43
50
  this.focusLast = false;
44
51
  this.restoreFocus();
45
52
  }
@@ -65,6 +72,37 @@ export default class extends Controller {
65
72
  this.stopPositioning = observeOverlayPosition(update);
66
73
  }
67
74
 
75
+ startOutsidePointerFallback() {
76
+ if (!this.supportsPopover || this.stopOutsidePointerFallbackListener)
77
+ return;
78
+
79
+ document.addEventListener("pointerdown", this.outsidePointerDown, true);
80
+ this.stopOutsidePointerFallbackListener = () => {
81
+ document.removeEventListener(
82
+ "pointerdown",
83
+ this.outsidePointerDown,
84
+ true,
85
+ );
86
+ this.stopOutsidePointerFallbackListener = null;
87
+ };
88
+ }
89
+
90
+ stopOutsidePointerFallback() {
91
+ this.stopOutsidePointerFallbackListener?.();
92
+ }
93
+
94
+ outsidePointerDown(event) {
95
+ if (!this.contentTarget.matches(":popover-open")) return;
96
+
97
+ const path = event.composedPath?.();
98
+ const insideMenu = path
99
+ ? path.includes(this.contentTarget) || path.includes(this.triggerTarget)
100
+ : this.contentTarget.contains(event.target) ||
101
+ this.triggerTarget.contains(event.target);
102
+
103
+ if (!insideMenu) this.hide();
104
+ }
105
+
68
106
  navigate(event) {
69
107
  switch (event.key) {
70
108
  case "Escape":
@@ -123,4 +161,11 @@ export default class extends Controller {
123
161
  (item) => !item.disabled && item.getAttribute("aria-disabled") !== "true",
124
162
  );
125
163
  }
164
+
165
+ get supportsPopover() {
166
+ return (
167
+ typeof this.contentTarget.showPopover === "function" &&
168
+ typeof this.contentTarget.hidePopover === "function"
169
+ );
170
+ }
126
171
  }
@@ -230,8 +230,8 @@ export default class extends Controller {
230
230
  this.element.dataset.state = this.initialInputDisabled
231
231
  ? "disabled"
232
232
  : selectedCount > 0
233
- ? "success"
234
- : "idle";
233
+ ? "success"
234
+ : "idle";
235
235
  }
236
236
 
237
237
  replaceFiles(files) {
@@ -24,6 +24,10 @@ export default class extends Controller {
24
24
  if (this.hasFallbackTarget) this.fallbackTarget.hidden = true;
25
25
  }
26
26
 
27
+ prepareForCache() {
28
+ delete this.element.dataset.enhanced;
29
+ }
30
+
27
31
  imageTargetConnected(image) {
28
32
  if (this.connected) this.bindImage(image);
29
33
  }
@@ -47,11 +51,14 @@ export default class extends Controller {
47
51
  this.releaseImage();
48
52
  this.boundImage = image;
49
53
  this.revision = (this.revision || 0) + 1;
50
- this.setState("loading");
51
54
  image.addEventListener("load", this.onLoad);
52
55
  image.addEventListener("error", this.onError);
53
56
 
54
- if (image.complete) this.reflectComplete(image);
57
+ if (image.complete) {
58
+ this.reflectComplete(image);
59
+ } else {
60
+ this.setState("loading");
61
+ }
55
62
  }
56
63
 
57
64
  releaseImage() {
@@ -98,11 +105,8 @@ export default class extends Controller {
98
105
  }
99
106
 
100
107
  reflectComplete(image) {
101
- if (image.naturalWidth > 0) {
102
- this.loaded();
103
- } else {
104
- this.failed();
105
- }
108
+ this.revision = (this.revision || 0) + 1;
109
+ this.setState(image.naturalWidth > 0 ? "loaded" : "error");
106
110
  }
107
111
 
108
112
  setState(state) {
@@ -43,9 +43,12 @@ export default class extends Controller {
43
43
  }
44
44
 
45
45
  navigate(event) {
46
+ const rightToLeft = getComputedStyle(this.element).direction === "rtl";
46
47
  const directionKeys =
47
48
  this.orientationValue === "vertical"
48
49
  ? ["ArrowUp", "ArrowDown"]
50
+ : rightToLeft
51
+ ? ["ArrowRight", "ArrowLeft"]
49
52
  : ["ArrowLeft", "ArrowRight"];
50
53
 
51
54
  if (![...directionKeys, "Home", "End"].includes(event.key)) return;
@@ -142,7 +145,11 @@ export default class extends Controller {
142
145
  const active = panel.dataset.key === value;
143
146
 
144
147
  panel.hidden = !active;
145
- panel.setAttribute("aria-hidden", String(!active));
148
+ if (active) {
149
+ panel.setAttribute("aria-hidden", "false");
150
+ } else {
151
+ panel.removeAttribute("aria-hidden");
152
+ }
146
153
  panel.dataset.state = active ? "active" : "inactive";
147
154
  panel.tabIndex = active ? 0 : -1;
148
155
  });
@@ -6,6 +6,8 @@ export default class extends Controller {
6
6
  }
7
7
 
8
8
  dismiss(event) {
9
+ if (!this.shown) return;
10
+
9
11
  event.preventDefault();
10
12
  this.element.dataset.dismissed = "";
11
13
  }
@@ -21,4 +23,11 @@ export default class extends Controller {
21
23
  reset() {
22
24
  delete this.element.dataset.dismissed;
23
25
  }
26
+
27
+ get shown() {
28
+ return (
29
+ !("dismissed" in this.element.dataset) &&
30
+ this.element.matches(":hover, :focus-within")
31
+ );
32
+ }
24
33
  }
@@ -12,7 +12,9 @@ en:
12
12
  dark: "Dark"
13
13
  system: "System"
14
14
  avatar_stack:
15
- overflow: "%{count} more avatars"
15
+ overflow:
16
+ one: "%{count} more avatar"
17
+ other: "%{count} more avatars"
16
18
  combobox:
17
19
  invalid_selection: "Choose an option."
18
20
  no_results: "No options found."
@@ -37,6 +39,7 @@ en:
37
39
  dropzone:
38
40
  label: "Upload files"
39
41
  prompt: "Drop files here or choose them from your device."
42
+ compact_prompt: "Drop or choose files."
40
43
  preview_list: "Selected files"
41
44
  progress: "Upload progress"
42
45
  progress_for: "Upload progress for %{name}"
data/docs/agent_guide.md CHANGED
@@ -20,8 +20,9 @@ Read this guide from that directory, then consult only the reference needed for
20
20
  | Choose or compose UI | `docs/component_contracts.md` |
21
21
  | Rails assets, forms, and application shells | `docs/rails_integration.md` |
22
22
  | Turbo, Frames, Streams, morphs, or Stimulus | `docs/hotwire.md` |
23
+ | Browser support or progressive fallback | `docs/browser_support.md` |
23
24
  | Theme tokens and application composition | `docs/customization.md` |
24
- | Upgrade an existing Nitro Kit 1.x app | `docs/migration_1_to_2.md` |
25
+ | Upgrade an existing Nitro Kit 1.x app | `docs/migration_1_to_2.md` |
25
26
  | Query, sort, filter, or paginate | `docs/patterns/queryable_collection.md` |
26
27
  | Create, update, and show validation | `docs/patterns/resource_form.md` |
27
28
  | Delete, revoke, archive, or confirm | `docs/patterns/destructive_action.md` |
@@ -34,21 +35,21 @@ The installed component source is the final authority for constructor and compou
34
35
 
35
36
  Prefer the component that owns the whole region, then compose smaller components inside it.
36
37
 
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` |
38
+ | Product need | Begin with |
39
+ | -------------------------------- | ------------------------------------------------------------- |
40
+ | Application chrome | `AppShell`, `AppNavigation`, `CommandPalette` |
41
+ | Authentication page | `AuthShell` |
42
+ | Settings navigation | `SettingsLayout` |
43
+ | Team or account administration | `AppShell`, `Toolbar`, `Table`, `SettingsSection` |
44
+ | App page title and basic actions | `AppShell`, then `Toolbar` |
45
+ | Content-led page introduction | `PageHeader` |
46
+ | Data region | `DataSection`, then `Table` or `EmptyState` |
47
+ | Queryable tabular data | `Table`, `Toolbar`, `PaginationBar` |
48
+ | Model-backed form | `SettingsSection`, Rails `form_with`, `NitroKit::FormBuilder` |
49
+ | Destructive settings | `DangerZone`, optionally `Dialog` |
50
+ | Transient server feedback | `Toast::FlashMessages` |
51
+ | Rendered Markdown or rich text | `Container`, then `Typeset` |
52
+ | General grouping | `Card`, `Flex`, `Grid`, `Container` |
52
53
 
53
54
  Application-specific product UI belongs under the application's namespace and composes Nitro components. Nitro owns component markup, styles, accessibility structure, and narrowly scoped progressive behavior. The application owns product policy, records, routes, authorization, queries, DOM IDs, and server responses.
54
55
 
@@ -70,6 +71,8 @@ After establishing that inventory, generate the host integration smoke tests
70
71
  from the currently bundled gem with `bin/rails generate
71
72
  nitro_kit:upgrade_smoke_tests`. The generator preserves existing files and
72
73
  skips missing Rails Minitest or system-test infrastructure with setup guidance.
74
+ Keep the generated explanatory header in each test: these are host-integration
75
+ regression checks, not substitutes for application-specific acceptance tests.
73
76
  Run supported generated tests before and after the application conversion,
74
77
  while retaining application-specific acceptance coverage for every inventoried
75
78
  flow. Customize each generated `prepare_nitro_kit_upgrade_smoke_test` method
@@ -89,6 +92,15 @@ Do not retain copied Nitro Kit 1.x source as the fallback. Remove the legacy
89
92
  component and preserve the unsupported behavior in clearly application-owned
90
93
  code. Report the missing equivalent as a Nitro Kit coverage gap.
91
94
 
95
+ At the end of a migration, search the whole application for
96
+ `desperately_need_a_class:` and review every result. Aim for zero rather than
97
+ translating old utility classes mechanically. Move application layout and
98
+ visual treatment to wrappers, use semantic component options or native
99
+ attributes, accept incidental Nitro defaults, and keep unmatched product UI
100
+ application-owned. Remove generic class forwarding from shared builders.
101
+ Retain an escape only for a named external integration that requires a class
102
+ hook, and record that reason in the migration review.
103
+
92
104
  ## Use one interaction grammar
93
105
 
94
106
  Choose the smallest primitive that completes the interaction:
@@ -119,6 +131,9 @@ Successful non-GET HTML submissions redirect with `303 See Other`. Invalid form
119
131
  inline or multi-column.
120
132
  - Use component options, compound declarations, layouts, and documented `--nk-*` theme properties.
121
133
  - Keep an HTML fallback for every Turbo form flow.
134
+ - Treat that HTML response as a Turbo transport fallback, not automatically a
135
+ JavaScript-free interaction; use `docs/browser_support.md` for the canonical
136
+ component classifications.
122
137
  - Test semantic output and stable owned attributes.
123
138
 
124
139
  Do not copy component source, add `nk_*` helpers, invent a general ERB bridge, mutate Nitro-owned Stimulus controllers, or pass `class:` and `style:`. The intentionally loud `desperately_need_a_class:` escape exists only for external integrations that require a class hook.
@@ -66,6 +66,20 @@ Rails remains first-class where Rails owns meaningful semantics:
66
66
 
67
67
  Applications include the `Phlex::Rails::Helpers::*` adapters they actually use. Nitro Kit does not recreate Rails helpers under an `nk_*` namespace.
68
68
 
69
+ ### Browser support
70
+
71
+ Nitro Kit uses modern web standards while keeping core content and actions
72
+ usable for the overwhelming majority of people on maintained browsers. The
73
+ practical target is current stable and popular evergreen Chrome, Edge, Firefox,
74
+ macOS Safari, and iOS Safari releases from roughly the two years before each
75
+ Nitro release. Mobile Safari is a first-class functional target. Nitro
76
+ feature-detects newer capabilities and owns the smallest fallback needed to
77
+ keep essential actions operable without holding components to the oldest
78
+ browser's feature set. Visual polish may degrade when the underlying content,
79
+ request path, semantics, and keyboard behavior remain intact. The versioned
80
+ public contract and no-JavaScript policy are in
81
+ [`browser_support.md`](browser_support.md).
82
+
69
83
  ## Consistency boundary
70
84
 
71
85
  The initial correctness layer is explicit Ruby APIs and immediate component validation, not a generalized page linter.
@@ -149,15 +163,18 @@ The stable release contains 40 atoms and components:
149
163
 
150
164
  The non-visual `AppearanceBootstrap` installs the shared document appearance runtime, and `NitroKit::Choice` is the typed option value shared by the choice controls.
151
165
 
152
- There is no Datepicker component and no date controller. `Input`'s `type: :date` is the only date control; `Field` and `FormBuilder` reach it through `as: :date`, and their CSS inherits the Safari date-editor alignment fix. `Combobox::Option` is likewise gone in favor of `Choice`.
166
+ There is no Datepicker component and no date controller. `Input`'s native date-family types are progressive enhancement; `Field` and `FormBuilder` preserve them, and `type: :date` inherits the Safari date-editor alignment fix. Month and week may render as text controls without picker, normalization, or constraint enforcement, so applications validate their ISO formats and ranges on the server and compose an application-owned `Select` when an exact bounded choice is required. `Combobox::Option` is likewise gone in favor of `Choice`.
153
167
 
154
168
  ### Native interaction authority
155
169
 
156
- Nitro uses current evergreen HTML primitives as the source of truth before adding JavaScript:
170
+ Nitro uses standards-based HTML primitives as the preferred path before adding
171
+ JavaScript. Native is not proof of compatibility: the primitive must cover the
172
+ browser-support window or Nitro must supply a focused fallback for essential
173
+ behavior.
157
174
 
158
- - Accordion items are native `details`/`summary` disclosures. Single mode uses one shared `name`; it has no controller or disabled-item abstraction.
159
- - Dialog declarations produce exactly one native panel through the required `panel(title:, description: nil, nonmodal: false)` declaration, and Nitro renders close button, title, description, then application content inside it. `command="show-modal"` and `command="close"` controls target the panel through `commandfor`; `nonmodal: true` is the only server-rendered open mode and cannot be combined with a trigger. `nk--dialog` adds only backdrop light dismissal and, for `dismissible: false`, Escape suppression.
160
- - Dropdown visibility and invoker state belong to `popover="auto"`. `trigger` forwards `icon:`, `icon_end:`, and `label:` to Button, and `item` accepts its own `icon:`. Its small controller supplies menu focus, arrow/Home/End navigation, and focus restoration to the trigger when the popover closes with focus still inside it. CSS anchor positioning follows the trigger when supported and otherwise centers the menu safely in the viewport.
175
+ - Accordion items are native `details`/`summary` disclosures. Single mode uses one shared `name` as its exclusive-group authority; where named-details grouping is unavailable, disclosure remains fully usable but the single-mode baseline is reduced to independently open items. Nitro deliberately adds no controller or browser detection for that short compatibility gap.
176
+ - Dialog declarations produce exactly one native panel through the required `panel(title:, description: nil, nonmodal: false)` declaration, and Nitro renders close button, title, description, then application content inside it. `command="show-modal"` and `command="close"` controls target the panel through `commandfor`; `nonmodal: true` is the only server-rendered open mode and cannot be combined with a trigger. `nk--dialog` handles backdrop light dismissal and, for `dismissible: false`, Escape suppression. Because Invoker Commands do not cover the full browser-support window, that controller also checks the reflected invoker relationship and falls back to `showModal()` or `close()` when the native command cannot run. The canonical no-JavaScript classifications remain in [`browser_support.md`](browser_support.md).
177
+ - Dropdown visibility and invoker state belong to `popover="auto"`. `trigger` forwards `icon:`, `icon_end:`, and `label:` to Button, and `item` accepts its own `icon:`. Its small controller supplies menu focus, arrow/Home/End navigation, focus restoration, collision-aware positioning, and a capability-checked outside-pointer dismissal fallback for WebKit Popover engines that miss light dismissal. The fallback requests the native close transition only while the popover is open, ignores composed-path interactions inside the menu or its trigger, and releases its listener on close or disconnect. Without Nitro JavaScript, native Popover still owns visibility; its bounded top-left placement is reduced from trigger-relative collision-aware placement.
161
178
  - CommandPalette uses one native dialog, a declarative search-shaped trigger, and native destination links. Its controller adds the optional Command-K/Control-K shortcut, local filtering, result announcements, and Turbo cleanup without replacing link navigation or retaining hidden application policy. With `search_url:`, the same input submits debounced GET requests into the owned Turbo Frame; the endpoint returns `CommandPalette::Results` HTML and remains responsible for authorization.
162
179
  - Tooltip visibility belongs to CSS hover and focus selectors, including a hoverable bridge across the visual gap. Button triggers cover ordinary buttons and links; `as: :custom` forwards owned HTML, ARIA, and data to an existing focusable mutation or compound trigger. Its controller only implements Escape dismissal and reset.
163
180
 
@@ -211,7 +228,7 @@ Eleven blocks and shells were extracted:
211
228
  - `PageHeader`
212
229
  - `StatGrid`
213
230
  - `DataSection`
214
- - `FormSection`
231
+ - `SettingsSection`
215
232
  - `DangerZone`
216
233
  - `EmptyState`
217
234
 
@@ -294,7 +311,7 @@ The audited former-Pro catalog maps completely to the new architecture:
294
311
 
295
312
  | Former surface | 2.0 disposition |
296
313
  | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
297
- | Sidebar and Top Navigation layouts | Rebuilt as `AppShell` for whole-application navigation and `Sheet` for contextual narrow panels |
314
+ | Sidebar and Top Navigation layouts | Rebuilt as `AppShell` for whole-application navigation and `Sheet` for contextual narrow panels |
298
315
  | Details Table | Rebuilt as `DetailsTable` |
299
316
  | Dropzone | Rebuilt as native `Dropzone`; Dropzone.js is removed |
300
317
  | Progressive Image | Rebuilt as `ProgressiveImage` |
@@ -338,7 +355,7 @@ nitro-kit.compound
338
355
 
339
356
  Selectors use `:where()` for zero authored specificity. Nitro ships its own global, Preflight-style reset in the `nitro-kit.reset` layer and never depends on Tailwind Preflight. It normalizes the whole page, so arbitrary content inside a component gets the same baseline as Nitro's own markup, and because it is layered, unlayered application CSS always wins. `min-width: 0` and list-marker removal stay scoped to Nitro-owned elements so prose lists keep real markers.
340
357
 
341
- Public `--nk-*` variables cover semantic colors, the component-specific raised default-button treatment, typography, spacing, radii, border and focus geometry, shadows, motion, control heights, and content widths. Private `--_nk-*` variables coordinate component mechanics and are not theme API.
358
+ Public `--nk-*` variables cover semantic colors, the component-specific raised default-button treatment and optional button-only radius, typography, spacing, shared radii, border and focus geometry, shadows, motion, control heights, and content widths. Private `--_nk-*` variables coordinate component mechanics and are not theme API.
342
359
 
343
360
  The exact browser order is optional third-party base CSS such as Lexxy, optional `nitro_kit-tailwind-v4.css`, generated `nitro_kit.css`, compiled Tailwind CSS when present, then unlayered application styles containing token overrides. `AppearanceBootstrap` precedes every stylesheet. Applications never edit the generated asset. [`customization.md`](customization.md) is the complete public token inventory and usage guide.
344
361
 
@@ -360,9 +377,9 @@ Turbo Frames and Turbo Streams remain Rails helpers used directly from Phlex. In
360
377
 
361
378
  Server-rendered feedback is the Rails flash. `Toast` renders `section[data-nk="toast"][role="region"]` wrapping `ol[data-slot="toast-list"]`, whose id is the toast id plus `-list`, so the default region is addressable as `nk-toast-list` and a Turbo Stream can append `NitroKit::Toast::Item` to it directly. Items carry `role="status"`, or `role="alert"` for the error variant, so a server-rendered item announces without waiting for a DOM mutation, and every item is `data-turbo-temporary` so a cached page never replays stale feedback while the region and list survive. `Toast::FlashMessages` maps an enumerable Rails flash onto the same items. Nitro does not add a client-side notification store.
362
379
 
363
- The engine ships CSS assets and Nitro-owned Stimulus controllers for enhancements that native HTML and CSS do not cover. When importmap is present it adds its pins automatically; the host still owns Stimulus and its normal controller loader. The engine boots without importmap: Accordion is complete with no controller at all and Dialog still opens and closes through declarative `command`/`commandfor`, while enhanced interactions such as Dropdown keyboard navigation, Tooltip Escape dismissal, and Dialog backdrop light dismissal require their pinned controllers. Nitro Kit 2.0 does not define a JavaScript-package entrypoint for automatic bundler registration.
380
+ The engine ships CSS assets and Nitro-owned Stimulus controllers for enhancements and compatibility bridges that native HTML and CSS do not cover throughout the support window. When importmap is present it adds its pins automatically; the host still owns Stimulus and its normal controller loader. The engine boots without importmap, but only the component-specific no-JavaScript baselines remain. Accordion keeps its full native disclosure baseline without a controller; its single mode is reduced to independently open items only where named-details grouping is unavailable. Dialog opens without its controller only where declarative `command`/`commandfor` is supported. Nitro Kit 2.0 does not define a JavaScript-package entrypoint for automatic bundler registration.
364
381
 
365
- Date inputs, Switch, and ordinary checked state deliberately use native inputs rather than custom controllers. The one exception is `indeterminate:`, which HTML cannot express as an attribute: `Checkbox` mounts `nk--checkable` only in that case, and the controller's whole job is to apply the native DOM property and own the matching `data-state="indeterminate"`. No third-party JavaScript runtime is vendored.
382
+ Date-family inputs, Switch, and ordinary checked state deliberately use native inputs rather than custom controllers. Month and week retain their native types as progressive enhancement even where a browser exposes text entry; Nitro does not promise a picker or client-side normalization. The one exception is `indeterminate:`, which HTML cannot express as an attribute: `Checkbox` mounts `nk--checkable` only in that case, and the controller's whole job is to apply the native DOM property and own the matching `data-state="indeterminate"`. No third-party JavaScript runtime is vendored.
366
383
 
367
384
  ## Examples and verification
368
385
 
@@ -0,0 +1,193 @@
1
+ # Browser support
2
+
3
+ Nitro Kit uses modern web standards while keeping core content and actions
4
+ usable for the overwhelming majority of people on maintained browsers.
5
+
6
+ The practical target is current stable and popular evergreen releases from
7
+ roughly the two years before each Nitro Kit release. This includes Chrome and
8
+ Edge on desktop, Chrome on Android, Firefox, Safari on macOS, and Safari on iOS.
9
+ It is a coverage target, not a promise of identical results in every version or
10
+ a reason to hold components to the oldest browser's feature set.
11
+
12
+ Each release's dated test matrix sets the concrete floor using release date and
13
+ real-world usage rather than an equal number of major versions. Mobile Safari
14
+ is a first-class target, not a reduced mobile tier. Important usage,
15
+ accessibility, or platform constraints may justify coverage outside the usual
16
+ window.
17
+
18
+ ## Functional baseline
19
+
20
+ In browsers covered by a release's matrix, Nitro components must keep their
21
+ essential content and actions usable. Forms must submit, links must navigate,
22
+ disclosures and overlays must open and close, destructive actions must retain a
23
+ usable request path, and controls must preserve their accessible names and
24
+ keyboard behavior.
25
+
26
+ Visual and convenience enhancements may degrade when a browser lacks a newer
27
+ CSS or platform feature. Exact animation, preferred overlay placement, advanced
28
+ text wrapping, and similar polish are not required when the simpler result
29
+ remains understandable and operable. Support means usable and accessible, not
30
+ pixel-identical presentation or identical convenience behavior.
31
+
32
+ Nitro prefers approved web standards and native browser behavior, but native is
33
+ not synonymous with supported. A newer standard can be adopted before it covers
34
+ the practical browser target when Nitro can preserve essential behavior:
35
+
36
+ 1. Keep the standard markup or API as the preferred path.
37
+ 2. Detect support by capability rather than user agent.
38
+ 3. Add the smallest Nitro-owned fallback needed to preserve functionality.
39
+ 4. Use a focused polyfill only when a local fallback cannot provide the
40
+ required semantics.
41
+
42
+ Typeset is a CSS-only exception to the usual progressive-enhancement note:
43
+ its readable source uses `@scope`, while the stylesheet also contains a
44
+ low-specificity fallback for engines that do not parse `@scope` (including
45
+ Firefox through 145). The fallback provides root typography and explicitly
46
+ anchored direct-child rules for headings, flow elements, lists, code/pre, and
47
+ tables, plus links within those supported semantic elements and their focus
48
+ state. It excludes direct nested
49
+ `[data-nk]` and `data-typeset="off"` boundaries and intentionally does not
50
+ promise the full descendant styling of the scoped path.
51
+
52
+ Applications should not need to copy Nitro controllers or install a general
53
+ polyfill bundle for a Nitro-owned component. Browsers outside a release's matrix
54
+ may still work, but Nitro does not promise fixes for them.
55
+
56
+ ### Month and week inputs
57
+
58
+ `Input(type: :month)` and `Input(type: :week)`, including the matching `Field`
59
+ and `FormBuilder` paths, retain the standard native input types as progressive
60
+ enhancement. Desktop Safari and Firefox can expose these controls as text
61
+ inputs, and native week selection arrived later on iOS Safari. In those cases a
62
+ picker, browser normalization, and enforcement of `min`, `max`, and `step` may
63
+ be absent.
64
+
65
+ Applications must accept and validate the submitted ISO shapes on the server:
66
+ `YYYY-MM` for month and `YYYY-Www` for ISO week. They must also validate allowed
67
+ ranges and increments as domain rules; the native constraint attributes are
68
+ client-side hints, not an authorization or validation boundary. When choosing
69
+ only from an exact, bounded set is essential, compose an application-owned
70
+ `Select` with explicit month or week options instead of relying on a generic
71
+ Nitro datepicker. Ordinary `date`, `time`, and `datetime-local` controls keep
72
+ their existing contracts.
73
+
74
+ ## JavaScript and progressive enhancement
75
+
76
+ Normal support assumes the documented Nitro JavaScript is installed. Nitro is
77
+ server-rendered and progressively enhanced, not JavaScript-free. Static content,
78
+ links, ordinary forms, and only the component-specific baselines documented in
79
+ the component contracts are guaranteed without JavaScript. Turbo transport,
80
+ Stimulus enhancements, and compatibility bridges are then unavailable.
81
+
82
+ The classifications below describe the rendered page with Nitro JavaScript,
83
+ Stimulus, and Turbo absent:
84
+
85
+ - **Full** means the component's essential content and action remain available;
86
+ documented convenience behavior may still be absent.
87
+ - **Reduced** means useful content or a simpler native interaction remains, but
88
+ part of the component's defining interaction is absent.
89
+ - **Unavailable** means the defining interaction cannot be completed. Critical
90
+ actions need an ordinary server-rendered route outside that component.
91
+
92
+ A server-rendered HTML response is not automatically a JavaScript-free
93
+ interaction. It can still place content in a closed dialog or rely on Turbo to
94
+ submit, replace, or confirm. Likewise, an HTML response branch is the fallback
95
+ for Turbo transport, not proof that the surrounding control works without
96
+ JavaScript.
97
+
98
+ ### No-JavaScript component matrix
99
+
100
+ | Component or family | Classification | Concise baseline without JavaScript |
101
+ | ---------------------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
102
+ | Links, Button, ButtonTo, and ordinary forms | Full | Native navigation and submission work. Submission indicators and Turbo transport or confirmation do not. |
103
+ | Input, Select, Textarea, Switch, RadioButton, and Checkbox | Full; reduced for indeterminate Checkbox | Native controls edit and submit. `indeterminate: true` cannot be applied because it is a DOM property; month/week have the separate native-control limitations below. |
104
+ | Accordion | Full; reduced single grouping where named `details` is unsupported | Every `details` opens and closes. Exclusive grouping depends on native shared-`name` support. |
105
+ | AppShell | Full | Navigation stays visible in document flow at narrow widths; the enhanced drawer interaction is absent. |
106
+ | Appearance | Reduced | CSS follows the system preference and server-rendered picker state remains visible, but a picker cannot apply or persist a new choice without the bootstrap runtime and controller. |
107
+ | Avatar | Reduced | The native image and rendered fallback remain, but a failed image is not swapped to its fallback. |
108
+ | Combobox | Full | The labelled native Select remains the named control and submission source; searchable listbox behavior is absent. |
109
+ | Dialog and Sheet | Reduced with Invoker Commands; unavailable without them | Declarative trigger and close controls work only with native Invoker Commands. A server-open nonmodal Dialog remains visible; controller dismissal policy and the compatibility bridge are absent. |
110
+ | CommandPalette | Reduced with Invoker Commands; unavailable without them | Native commands can open the dialog and its destination links navigate, but search, shortcut, announcements, and Turbo results are absent. Without Invoker Commands the closed content is unreachable. |
111
+ | Dropdown | Reduced | Native Popover opens, closes, and light-dismisses. Menu keyboard conventions, focus restoration, collision-aware placement, and Nitro's outside-pointer compatibility fallback are absent; CSS supplies bounded top-left placement. |
112
+ | Dropzone | Full | Its labelled native file input and ordinary form submission work; drag preview, policy feedback, progress, and direct upload are absent. |
113
+ | RichTextArea | Unavailable unless the host editor provides its own fallback | Nitro wraps trusted host-editor markup and does not manufacture a plain-text control. The host editor owns its JavaScript and fallback contract. |
114
+ | ProgressiveImage | Reduced | The native image loads and retains its accessible alternative; decode/load state and the visible error fallback are not enhanced. |
115
+ | Tabs | Reduced | Every panel and its heading control remain visible and reachable; single-panel selection and APG keyboard behavior are absent. |
116
+ | Toast | Reduced | Server-rendered flash content and live-region semantics remain; timed and manual dismissal are absent. |
117
+ | Tooltip | Reduced | CSS hover and focus disclosure remain; Escape dismissal is absent. |
118
+
119
+ Typeset is not interactive, but its no-JavaScript and no-`@scope` CSS path is
120
+ also reduced: the fallback covers the documented semantic subset rather than
121
+ full descendant parity.
122
+
123
+ ### Compatibility details
124
+
125
+ #### Accordion
126
+
127
+ Accordion always keeps native `details` and `summary` as the disclosure
128
+ authority. Its no-JavaScript baseline is full for opening and closing each
129
+ item. In `single` mode, a shared `name` is the browser-native exclusive-group
130
+ mechanism: browsers that support named details provide the full one-open-item
131
+ behavior without a Nitro controller.
132
+
133
+ The short Firefox gap before Firefox 130, including Firefox 128 ESR, has a
134
+ reduced `single` baseline: each disclosure still opens and closes normally,
135
+ but more than one item can remain open. Nitro does not detect the browser or
136
+ install JavaScript merely to duplicate that native grouping. Applications for
137
+ which strict one-open-at-a-time behavior is essential in that gap should use a
138
+ different interaction with an application-owned server or JavaScript policy.
139
+
140
+ Browser compatibility is verified against current stable browsers and
141
+ representative older releases near the edge of the support window. Compatibility
142
+ work prioritizes real Safari and iOS Safari coverage in addition to Chromium;
143
+ tests that merely remove an attribute in current Chrome are useful branch tests,
144
+ but are not a substitute for the affected browser engine.
145
+
146
+ Dialog, Sheet, and CommandPalette keep declarative `command`/`commandfor`
147
+ controls in their server markup. With Nitro JavaScript installed, their shared
148
+ controller uses the reflected invoker relationship when available and falls
149
+ back to `HTMLDialogElement.showModal()` or `close()` only when that relationship
150
+ cannot run. Without JavaScript, opening and closing these overlays is available
151
+ only in browsers with Invoker Commands; destination links and ordinary forms
152
+ remain server-rendered, but content inside a closed dialog is not reachable.
153
+
154
+ Each Nitro release records a dated tested-browser matrix. The rolling policy is
155
+ the durable contract; the matrix records the concrete versions used to verify a
156
+ particular release.
157
+
158
+ ## Nitro Kit 2.0 release matrix (2026-08-05)
159
+
160
+ This is the release checklist for the next Nitro 2.0 release. “Automated” means
161
+ the Rails/Selenium system suite runs in CI; it does not claim that Selenium
162
+ reproduces every device or browser UI detail. Version numbers are the stable
163
+ targets recorded on this date, and should be refreshed if the release slips.
164
+
165
+ | Target | Near-floor reference | Current release verification | Method |
166
+ | --------------- | -------------------- | ---------------------------- | ----------------------------------------------------- |
167
+ | Chrome desktop | Chrome 128 | Chrome 151 | Automated Ubuntu full-system-suite lane |
168
+ | Edge desktop | Edge 128 | Edge 151 | Manual Chromium release check (same engine as Chrome) |
169
+ | Chrome Android | Chrome 128 | Chrome 151 on Android | Manual real-device check |
170
+ | Firefox desktop | Firefox 128 ESR | Firefox 153 | Automated Ubuntu priority-smoke lane |
171
+ | Safari macOS | Safari 18 | Safari 26.5 | Automated `macos-latest` priority-smoke lane |
172
+ | Safari iOS | Safari 18 / iOS 18 | Safari 26.5 / iOS 26.5 | Manual real-device release check |
173
+
174
+ The automated matrix intentionally tests current Chrome, Firefox, and macOS
175
+ Safari rather than downloading historical binaries. Chrome runs the full
176
+ system suite. Firefox and Safari run `bin/browser-smoke`, the single maintained
177
+ priority lane for Dialog, Sheet, CommandPalette, Dropdown/AppearancePicker,
178
+ Accordion, Hotwire lifecycle, Typeset contracts, and date-family input
179
+ behavior. Chrome DevTools emulation remains Chrome-only coverage and is not
180
+ presented as cross-engine verification. Priority smoke lanes run with one
181
+ worker; Chrome retains the full suite's normal parallelism.
182
+
183
+ Before release, manually repeat those flows on one current Android Chrome and
184
+ one current iPhone Safari, including narrow layout, VoiceOver/TalkBack where
185
+ available, real form submission, and month/week ISO values. Record device OS,
186
+ browser build, date, and any reduced-baseline behavior in the release notes.
187
+ Do not describe the near-floor or iOS rows as automated coverage.
188
+
189
+ The Firefox 128 ESR and older Safari behavior that cannot be installed
190
+ reliably on GitHub-hosted runners is represented by capability-focused tests:
191
+ stripping Invoker Commands and exercising Nitro fallbacks, plus the documented
192
+ native month/week degradation. These branch simulations supplement, but do not
193
+ replace, the dated real-browser checks.