nitro_kit 0.9.0 → 2.0.0.alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/plugins/marketplace.json +20 -0
  3. data/CHANGELOG.md +172 -0
  4. data/LICENSE +28 -0
  5. data/README.md +22 -15
  6. data/STYLE_GUIDE.md +425 -0
  7. data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
  8. data/app/assets/stylesheets/nitro_kit.css +7239 -0
  9. data/app/components/nitro_kit/accordion.rb +125 -65
  10. data/app/components/nitro_kit/alert.rb +80 -43
  11. data/app/components/nitro_kit/app_navigation.rb +307 -0
  12. data/app/components/nitro_kit/app_shell.rb +227 -0
  13. data/app/components/nitro_kit/appearance_bootstrap.rb +146 -0
  14. data/app/components/nitro_kit/appearance_picker.rb +156 -0
  15. data/app/components/nitro_kit/auth_shell.rb +34 -0
  16. data/app/components/nitro_kit/avatar.rb +85 -29
  17. data/app/components/nitro_kit/avatar_stack.rb +155 -9
  18. data/app/components/nitro_kit/badge.rb +43 -81
  19. data/app/components/nitro_kit/button.rb +180 -103
  20. data/app/components/nitro_kit/button_group.rb +63 -11
  21. data/app/components/nitro_kit/button_to.rb +69 -0
  22. data/app/components/nitro_kit/card.rb +42 -45
  23. data/app/components/nitro_kit/checkbox.rb +115 -62
  24. data/app/components/nitro_kit/checkbox_group.rb +131 -22
  25. data/app/components/nitro_kit/choice.rb +55 -0
  26. data/app/components/nitro_kit/combobox.rb +292 -105
  27. data/app/components/nitro_kit/command_palette.rb +412 -0
  28. data/app/components/nitro_kit/component.rb +322 -46
  29. data/app/components/nitro_kit/container.rb +32 -0
  30. data/app/components/nitro_kit/control_group.rb +45 -0
  31. data/app/components/nitro_kit/danger_zone.rb +78 -0
  32. data/app/components/nitro_kit/data_section.rb +106 -0
  33. data/app/components/nitro_kit/details_table.rb +229 -0
  34. data/app/components/nitro_kit/dialog.rb +257 -83
  35. data/app/components/nitro_kit/dropdown.rb +335 -122
  36. data/app/components/nitro_kit/dropzone.rb +326 -0
  37. data/app/components/nitro_kit/empty_state.rb +99 -0
  38. data/app/components/nitro_kit/field.rb +461 -236
  39. data/app/components/nitro_kit/field_group.rb +15 -8
  40. data/app/components/nitro_kit/fieldset.rb +37 -39
  41. data/app/components/nitro_kit/flex.rb +49 -0
  42. data/app/components/nitro_kit/form_builder.rb +313 -72
  43. data/app/components/nitro_kit/form_section.rb +79 -0
  44. data/app/components/nitro_kit/grid.rb +34 -0
  45. data/app/components/nitro_kit/icon.rb +54 -25
  46. data/app/components/nitro_kit/input.rb +97 -10
  47. data/app/components/nitro_kit/label.rb +22 -6
  48. data/app/components/nitro_kit/layout_options.rb +7 -0
  49. data/app/components/nitro_kit/page_header.rb +76 -0
  50. data/app/components/nitro_kit/pagination.rb +355 -69
  51. data/app/components/nitro_kit/pagination_bar.rb +90 -0
  52. data/app/components/nitro_kit/progressive_image.rb +181 -0
  53. data/app/components/nitro_kit/radio_button.rb +77 -55
  54. data/app/components/nitro_kit/radio_button_group.rb +107 -36
  55. data/app/components/nitro_kit/responsive_value.rb +94 -0
  56. data/app/components/nitro_kit/rich_text_area.rb +31 -0
  57. data/app/components/nitro_kit/select.rb +129 -64
  58. data/app/components/nitro_kit/settings_layout.rb +152 -0
  59. data/app/components/nitro_kit/sheet.rb +202 -0
  60. data/app/components/nitro_kit/stat_grid.rb +75 -0
  61. data/app/components/nitro_kit/switch.rb +108 -51
  62. data/app/components/nitro_kit/table.rb +237 -45
  63. data/app/components/nitro_kit/tabs.rb +168 -63
  64. data/app/components/nitro_kit/textarea.rb +81 -10
  65. data/app/components/nitro_kit/toast.rb +211 -80
  66. data/app/components/nitro_kit/toolbar.rb +75 -0
  67. data/app/components/nitro_kit/tooltip.rb +235 -34
  68. data/app/components/nitro_kit/typeset.rb +28 -0
  69. data/app/javascript/controllers/nk/app_shell_controller.js +224 -0
  70. data/app/javascript/controllers/nk/appearance_controller.js +73 -0
  71. data/app/javascript/controllers/nk/avatar_controller.js +15 -0
  72. data/app/javascript/controllers/nk/button_controller.js +56 -0
  73. data/app/javascript/controllers/nk/checkable_controller.js +41 -0
  74. data/app/javascript/controllers/nk/combobox_controller.js +314 -82
  75. data/app/javascript/controllers/nk/command_palette_controller.js +249 -0
  76. data/app/javascript/controllers/nk/dialog_controller.js +14 -10
  77. data/app/javascript/controllers/nk/dropdown_controller.js +106 -58
  78. data/app/javascript/controllers/nk/dropzone/direct_upload.js +61 -0
  79. data/app/javascript/controllers/nk/dropzone/file_rules.js +77 -0
  80. data/app/javascript/controllers/nk/dropzone/form_submit_lock.js +30 -0
  81. data/app/javascript/controllers/nk/dropzone_controller.js +482 -0
  82. data/app/javascript/controllers/nk/overlay_position.js +37 -0
  83. data/app/javascript/controllers/nk/progressive_image_controller.js +116 -0
  84. data/app/javascript/controllers/nk/tabs_controller.js +135 -23
  85. data/app/javascript/controllers/nk/toast_controller.js +112 -45
  86. data/app/javascript/controllers/nk/tooltip_controller.js +12 -47
  87. data/config/importmap.rb +2 -0
  88. data/config/locales/en.yml +98 -0
  89. data/docs/agent_guide.md +139 -0
  90. data/docs/agent_native_spec.md +422 -0
  91. data/docs/component_contracts.md +263 -0
  92. data/docs/customization.md +595 -0
  93. data/docs/hotwire.md +94 -0
  94. data/docs/initialization_prompt.md +36 -0
  95. data/docs/migration_1_to_2.md +318 -0
  96. data/docs/new_app_strategy.md +22 -0
  97. data/docs/patterns/application_foundation.md +136 -0
  98. data/docs/patterns/crud_resource.md +144 -0
  99. data/docs/patterns/destructive_action.md +105 -0
  100. data/docs/patterns/flash_and_toast.md +57 -0
  101. data/docs/patterns/inline_edit.md +59 -0
  102. data/docs/patterns/queryable_collection.md +187 -0
  103. data/docs/patterns/resource_form.md +126 -0
  104. data/docs/rails_conventions.md +95 -0
  105. data/docs/rails_integration.md +514 -0
  106. data/lib/generators/nitro_kit/install_generator.rb +48 -0
  107. data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +67 -0
  108. data/lib/nitro_kit/engine.rb +20 -0
  109. data/lib/nitro_kit/installation.rb +671 -0
  110. data/lib/nitro_kit/migration_inventory.rb +345 -0
  111. data/lib/nitro_kit/upgrade_smoke_test.rb +403 -0
  112. data/lib/nitro_kit/version.rb +1 -1
  113. data/lib/nitro_kit.rb +5 -42
  114. data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +42 -0
  115. data/lib/tasks/nitro_kit_tasks.rake +79 -4
  116. data/plugins/nitro-kit/.codex-plugin/plugin.json +28 -0
  117. data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +67 -0
  118. data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +4 -0
  119. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +60 -0
  120. data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +4 -0
  121. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +62 -0
  122. data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +4 -0
  123. data/src/stylesheets/nitro_kit/components/accordion.css +119 -0
  124. data/src/stylesheets/nitro_kit/components/alert.css +82 -0
  125. data/src/stylesheets/nitro_kit/components/app_navigation.css +156 -0
  126. data/src/stylesheets/nitro_kit/components/app_shell.css +605 -0
  127. data/src/stylesheets/nitro_kit/components/appearance_picker.css +117 -0
  128. data/src/stylesheets/nitro_kit/components/auth_shell.css +8 -0
  129. data/src/stylesheets/nitro_kit/components/avatar.css +81 -0
  130. data/src/stylesheets/nitro_kit/components/avatar_stack.css +80 -0
  131. data/src/stylesheets/nitro_kit/components/badge.css +76 -0
  132. data/src/stylesheets/nitro_kit/components/button.css +283 -0
  133. data/src/stylesheets/nitro_kit/components/button_group.css +53 -0
  134. data/src/stylesheets/nitro_kit/components/button_to.css +6 -0
  135. data/src/stylesheets/nitro_kit/components/card.css +70 -0
  136. data/src/stylesheets/nitro_kit/components/checkbox.css +160 -0
  137. data/src/stylesheets/nitro_kit/components/checkbox_group.css +61 -0
  138. data/src/stylesheets/nitro_kit/components/combobox.css +148 -0
  139. data/src/stylesheets/nitro_kit/components/command_palette.css +255 -0
  140. data/src/stylesheets/nitro_kit/components/container.css +25 -0
  141. data/src/stylesheets/nitro_kit/components/control_group.css +149 -0
  142. data/src/stylesheets/nitro_kit/components/danger_zone.css +54 -0
  143. data/src/stylesheets/nitro_kit/components/data_section.css +40 -0
  144. data/src/stylesheets/nitro_kit/components/details_table.css +53 -0
  145. data/src/stylesheets/nitro_kit/components/dialog.css +80 -0
  146. data/src/stylesheets/nitro_kit/components/dropdown.css +152 -0
  147. data/src/stylesheets/nitro_kit/components/dropzone.css +198 -0
  148. data/src/stylesheets/nitro_kit/components/empty_state.css +53 -0
  149. data/src/stylesheets/nitro_kit/components/field.css +79 -0
  150. data/src/stylesheets/nitro_kit/components/field_group.css +6 -0
  151. data/src/stylesheets/nitro_kit/components/fieldset.css +28 -0
  152. data/src/stylesheets/nitro_kit/components/flex.css +455 -0
  153. data/src/stylesheets/nitro_kit/components/form_section.css +71 -0
  154. data/src/stylesheets/nitro_kit/components/grid.css +308 -0
  155. data/src/stylesheets/nitro_kit/components/icon.css +26 -0
  156. data/src/stylesheets/nitro_kit/components/input.css +80 -0
  157. data/src/stylesheets/nitro_kit/components/label.css +9 -0
  158. data/src/stylesheets/nitro_kit/components/layout.css +440 -0
  159. data/src/stylesheets/nitro_kit/components/page_header.css +61 -0
  160. data/src/stylesheets/nitro_kit/components/pagination.css +119 -0
  161. data/src/stylesheets/nitro_kit/components/pagination_bar.css +41 -0
  162. data/src/stylesheets/nitro_kit/components/palette.css +113 -0
  163. data/src/stylesheets/nitro_kit/components/progressive_image.css +139 -0
  164. data/src/stylesheets/nitro_kit/components/radio_button.css +127 -0
  165. data/src/stylesheets/nitro_kit/components/radio_button_group.css +143 -0
  166. data/src/stylesheets/nitro_kit/components/rich_text_area.css +21 -0
  167. data/src/stylesheets/nitro_kit/components/select.css +70 -0
  168. data/src/stylesheets/nitro_kit/components/settings_layout.css +100 -0
  169. data/src/stylesheets/nitro_kit/components/sheet.css +100 -0
  170. data/src/stylesheets/nitro_kit/components/stat_grid.css +51 -0
  171. data/src/stylesheets/nitro_kit/components/switch.css +152 -0
  172. data/src/stylesheets/nitro_kit/components/table.css +116 -0
  173. data/src/stylesheets/nitro_kit/components/tabs.css +138 -0
  174. data/src/stylesheets/nitro_kit/components/textarea.css +39 -0
  175. data/src/stylesheets/nitro_kit/components/toast.css +131 -0
  176. data/src/stylesheets/nitro_kit/components/toolbar.css +78 -0
  177. data/src/stylesheets/nitro_kit/components/tooltip.css +124 -0
  178. data/src/stylesheets/nitro_kit/components/typeset.css +191 -0
  179. data/src/stylesheets/nitro_kit/layers.css +7 -0
  180. data/src/stylesheets/nitro_kit/reset.css +192 -0
  181. data/src/stylesheets/nitro_kit/tokens.css +198 -0
  182. metadata +182 -51
  183. data/Rakefile +0 -8
  184. data/app/assets/tailwind/application.css +0 -49
  185. data/app/components/nitro_kit/datepicker.rb +0 -9
  186. data/app/helpers/nitro_kit/accordion_helper.rb +0 -9
  187. data/app/helpers/nitro_kit/alert_helper.rb +0 -11
  188. data/app/helpers/nitro_kit/avatar_helper.rb +0 -13
  189. data/app/helpers/nitro_kit/badge_helper.rb +0 -13
  190. data/app/helpers/nitro_kit/button_group_helper.rb +0 -9
  191. data/app/helpers/nitro_kit/button_helper.rb +0 -49
  192. data/app/helpers/nitro_kit/card_helper.rb +0 -9
  193. data/app/helpers/nitro_kit/checkbox_helper.rb +0 -50
  194. data/app/helpers/nitro_kit/combobox_helper.rb +0 -9
  195. data/app/helpers/nitro_kit/datepicker_helper.rb +0 -9
  196. data/app/helpers/nitro_kit/dialog_helper.rb +0 -9
  197. data/app/helpers/nitro_kit/dropdown_helper.rb +0 -9
  198. data/app/helpers/nitro_kit/field_group_helper.rb +0 -9
  199. data/app/helpers/nitro_kit/field_helper.rb +0 -9
  200. data/app/helpers/nitro_kit/fieldset_helper.rb +0 -9
  201. data/app/helpers/nitro_kit/form_helper.rb +0 -13
  202. data/app/helpers/nitro_kit/icon_helper.rb +0 -9
  203. data/app/helpers/nitro_kit/input_helper.rb +0 -37
  204. data/app/helpers/nitro_kit/label_helper.rb +0 -19
  205. data/app/helpers/nitro_kit/pagination_helper.rb +0 -46
  206. data/app/helpers/nitro_kit/radio_button_helper.rb +0 -23
  207. data/app/helpers/nitro_kit/select_helper.rb +0 -24
  208. data/app/helpers/nitro_kit/switch_helper.rb +0 -9
  209. data/app/helpers/nitro_kit/table_helper.rb +0 -9
  210. data/app/helpers/nitro_kit/tabs_helper.rb +0 -9
  211. data/app/helpers/nitro_kit/textarea_helper.rb +0 -9
  212. data/app/helpers/nitro_kit/toast_helper.rb +0 -25
  213. data/app/helpers/nitro_kit/tooltip_helper.rb +0 -9
  214. data/app/javascript/controllers/nk/accordion_controller.js +0 -18
  215. data/app/javascript/controllers/nk/datepicker_controller.js +0 -5
  216. data/app/javascript/controllers/nk/switch_controller.js +0 -32
  217. data/lib/generators/nitro_kit/component_generator.rb +0 -84
  218. data/lib/nitro_kit/schema_builder.rb +0 -129
  219. data/lib/nitro_kit/variants.rb +0 -21
data/STYLE_GUIDE.md ADDED
@@ -0,0 +1,425 @@
1
+ # Nitro Kit 2.0 component style guide
2
+
3
+ Nitro Kit is a gem-owned, Phlex-only UI system for Rails. Its public surface is typed Ruby composition, self-describing markup, and static CSS driven by custom properties.
4
+
5
+ `NitroKit` extends `Phlex::Kit`. Applications should include it once in their
6
+ base Phlex component and use capitalized Kit methods as the primary composition
7
+ syntax:
8
+
9
+ ```ruby
10
+ class ApplicationComponent < Phlex::HTML
11
+ include NitroKit
12
+ end
13
+
14
+ class SaveButton < ApplicationComponent
15
+ def view_template
16
+ Button("Save", variant: :primary)
17
+ end
18
+ end
19
+ ```
20
+
21
+ The explicit `render NitroKit::Button.new(...)` form remains supported and is
22
+ required when constructing a component object for another API. Kit methods
23
+ render immediately and only work from a Phlex context; they do not work in ERB.
24
+
25
+ ## Principles
26
+
27
+ - Prefer the smallest obvious Ruby API.
28
+ - Compose components directly with Phlex.
29
+ - Make invalid component vocabulary impossible to render silently.
30
+ - Keep state and component identity visible in markup.
31
+ - Preserve native HTML and Rails semantics.
32
+ - Let applications customize themes and compose product-specific UI without editing Nitro internals.
33
+ - Keep behavior minimal, progressive, and Turbo-safe.
34
+
35
+ ## File layout
36
+
37
+ ```text
38
+ app/components/nitro_kit/ # gem-owned atoms, layouts, blocks
39
+ app/javascript/controllers/nk/ # gem-owned Stimulus behavior
40
+ src/stylesheets/nitro_kit/ # plain CSS authoring sources
41
+ app/assets/stylesheets/ # generated browser-ready distribution CSS
42
+ test/components/ # focused render/contract tests
43
+ test/dummy/app/components/gallery/ # Phlex gallery pages
44
+ test/integration/ # catalog-driven route coverage
45
+ ```
46
+
47
+ Do not add component helper modules or copied-component generators.
48
+
49
+ ## Component anatomy
50
+
51
+ Use explicit public keywords and pass an internal attribute bundle to the base component:
52
+
53
+ ```ruby
54
+ module NitroKit
55
+ class Container < Component
56
+ SIZES = %i[sm md lg xl].freeze
57
+
58
+ def initialize(
59
+ size:,
60
+ id: nil,
61
+ html: {},
62
+ aria: {},
63
+ data: {},
64
+ desperately_need_a_class: nil
65
+ )
66
+ size = validate_choice!(:size, size, SIZES)
67
+
68
+ super(
69
+ component: :container,
70
+ attributes: { id: }.compact,
71
+ html:,
72
+ aria:,
73
+ data:,
74
+ size:,
75
+ desperately_need_a_class:
76
+ )
77
+ end
78
+
79
+ def view_template
80
+ div(**root_attributes) { yield if block_given? }
81
+ end
82
+ end
83
+ end
84
+ ```
85
+
86
+ These base boundaries are settled for 2.0: explicit component options, deliberate native-attribute bags, reserved Nitro identity, validated closed vocabularies, and one centralized class escape hatch.
87
+
88
+ ## Options and native attributes
89
+
90
+ Component semantics are explicit keywords. Do not use a broad `**attrs` or `**options` public argument.
91
+
92
+ Common element semantics can be first-class keywords when central to the component: `id:`, `href:`, `type:`, `name:`, `value:`, `disabled:`, `required:`, and similar.
93
+
94
+ Less common native attributes use:
95
+
96
+ - `html:` for ordinary attributes.
97
+ - `aria:` for ARIA attributes.
98
+ - `data:` for non-reserved application data and additive Stimulus controllers/actions.
99
+
100
+ Reject `class` and `style`, including nested in `html:`. Reject every spelling of Nitro-reserved data keys, including symbol/string and dashed/underscored forms.
101
+
102
+ Nitro-owned data cannot be replaced through the public boundary. Collisions raise, except user Stimulus `controller` and `action` values, which compose deterministically with Nitro-owned values.
103
+
104
+ `Component::COMPONENT_OWNED_DATA_ATTRIBUTES` lists the keys a component writes for itself through `attributes:` — `state`, `disabled`, `required`, `orientation`, `presentation`, `placement`, `layout`, and `field-type`. `Component::RESERVED_DATA_ATTRIBUTES` adds `nk`, `slot`, `variant`, `size`, `nk-escape`, and `enhanced`. Everything in the combined list is rejected from `data:`; the component-owned subset is the part a component may still set internally.
105
+
106
+ ### Variant is an identity axis, not a style hook
107
+
108
+ A component root's `variant:` and `size:` are its identity axes, and only the base component emits them. Pass them through `super(variant:, size:)`; never write `data-variant` or `data-size` by hand on a root.
109
+
110
+ A slot may carry its own owned `data-variant` when the slot has variant identity of its own rather than merely inheriting the root's. Two precedents settle the shape:
111
+
112
+ - `Toast::Item` is a nested component. Its variant reaches `data-variant` through the ordinary root channel, and the parent attaches it to the `toast-item` slot.
113
+ - Dropdown `item` is a plain element, so it uses the base component's `slot_attributes(:item, variant:)` channel, which normalizes and owns the value exactly as a root does.
114
+
115
+ Use `slot_attributes(..., variant:)`; do not hand-merge a `variant` key into a slot's data bag. Both remain Nitro-owned: caller `data: { variant: }` is reserved and raises before it can reach a root or a slot. Do not introduce a slot `data-variant` that only restates the root's variant, and do not accept a public `variant:` on a slot that has no closed vocabulary of its own.
116
+
117
+ ## Validation
118
+
119
+ Validate every closed vocabulary at construction time:
120
+
121
+ ```ruby
122
+ @variant = validate_choice!(:variant, variant, VARIANTS)
123
+ ```
124
+
125
+ Errors identify the invalid option or slot, the received value where useful, and the accepted vocabulary. Do not silently fall back.
126
+
127
+ Required slots and invalid slot combinations should raise as soon as the component can know them. Do not attempt to validate whole-page information architecture in the component kernel.
128
+
129
+ ## Identity and slots
130
+
131
+ Every component root emits `data-nk`:
132
+
133
+ ```html
134
+ <article data-nk="card"></article>
135
+ ```
136
+
137
+ Every owned part has a component-qualified slot:
138
+
139
+ ```html
140
+ <article data-nk="card">
141
+ <h2 data-slot="card-title">...</h2>
142
+ <div data-slot="card-body">...</div>
143
+ </article>
144
+ ```
145
+
146
+ A nested component can have both identities:
147
+
148
+ ```html
149
+ <input data-nk="input" data-slot="field-control" />
150
+ ```
151
+
152
+ The parent supplies contextual slot identity. An atom such as `Label` must not globally identify itself as every parent's `label` slot.
153
+
154
+ Prefer direct-child contracts for component-owned structure. Do not style arbitrary application descendants merely because they appear inside a content slot.
155
+
156
+ ## Compound components and content
157
+
158
+ Direct Phlex Kit composition replaces the old template-aware `builder do`
159
+ wrapper. Compound APIs should be ordinary Ruby methods that render into the
160
+ current Phlex context.
161
+
162
+ Named leaf slots may accept arbitrary application content. That is normal composition, not an escape.
163
+
164
+ When a fixed-order block exposes textual constructor keywords, expose matching deferred compound methods too:
165
+
166
+ ```ruby
167
+ EmptyState(level: 3) do |empty|
168
+ empty.title { plain "No records for "; strong { "Production" } }
169
+ empty.description("Remove one or more filters and try again.")
170
+ end
171
+ ```
172
+
173
+ Constructor text and the matching compound method are two forms of the same region. Accept either one, reject both or repeated declarations, and let required regions be satisfied by either form. Constructor values and compound-method text remain non-blank strings; a compound-method block may render arbitrary Phlex content. Collect these declarations before rendering so the component's owned DOM order does not depend on caller order.
174
+
175
+ Do not add an untyped structural bypass. If a legitimate application-content boundary is missing, add the smallest named compound method supported by real composition evidence.
176
+
177
+ ## Internationalization
178
+
179
+ Nitro owns no hardcoded user-facing English. Every string a person can read or
180
+ hear — visible copy, ARIA names, live-region announcements, validation
181
+ messages — comes from `config/locales/en.yml`, which the engine loads
182
+ automatically and which is the single authoritative source of the shipped
183
+ copy.
184
+
185
+ Components call `I18n.t` with a fully qualified key and no `:default`:
186
+
187
+ ```ruby
188
+ I18n.t("nitro_kit.dropzone.prompt")
189
+ ```
190
+
191
+ Do not duplicate copy into a `default:` argument or a Ruby constant; the
192
+ locale file would drift from it. Keys live under one `nitro_kit:` namespace and
193
+ are grouped by component (`nitro_kit.<component>.<key>`), with nested `status:`
194
+ and `errors:` groups where a component has many. Counted strings use ordinary
195
+ `one`/`other` pluralization and `%{...}` interpolation.
196
+
197
+ Translated text that is a public option stays a public option. Resolve the
198
+ default in the keyword itself so an application override still wins:
199
+
200
+ ```ruby
201
+ def initialize(label: I18n.t("nitro_kit.pagination.label"))
202
+ ```
203
+
204
+ Stimulus controllers never contain user-facing English as behavior. The Ruby
205
+ component translates each string the controller needs and emits it through the
206
+ Stimulus values API:
207
+
208
+ ```html
209
+ <div data-nk--dropzone-queued-value="Queued">
210
+ ```
211
+
212
+ The controller reads the value and keeps the shipped English literal only as an
213
+ inert fallback for markup assembled without the component. Runtime
214
+ interpolation uses the same `%{name}` placeholders the locale file declares, so
215
+ a translator sees one grammar in both languages.
216
+
217
+ ## Class escape
218
+
219
+ Nitro components never emit or depend on classes. The only exception is:
220
+
221
+ ```ruby
222
+ desperately_need_a_class: "external-widget-hook"
223
+ ```
224
+
225
+ It must produce both the class and `data-nk-escape="class"`. Blank or non-string values raise. Implement this once in the base component.
226
+
227
+ ## CSS architecture
228
+
229
+ Author plain CSS in split source files and generate one committed `nitro_kit.css` distribution asset.
230
+
231
+ Declare deterministic layers:
232
+
233
+ ```css
234
+ @layer nitro-kit.tokens,
235
+ nitro-kit.reset,
236
+ nitro-kit.base,
237
+ nitro-kit.variant,
238
+ nitro-kit.size,
239
+ nitro-kit.state,
240
+ nitro-kit.compound;
241
+ ```
242
+
243
+ Every Nitro selector uses `:where()`:
244
+
245
+ ```css
246
+ @layer nitro-kit.base {
247
+ :where([data-nk="button"]) {
248
+ background: var(--_nk-button-background);
249
+ }
250
+ }
251
+ ```
252
+
253
+ Variants assign private values and generic state consumes them:
254
+
255
+ ```css
256
+ @layer nitro-kit.variant {
257
+ :where([data-nk="button"][data-variant="primary"]) {
258
+ --_nk-button-background: var(--nk-color-primary);
259
+ --_nk-button-hover-background: var(--nk-color-primary-hover);
260
+ }
261
+ }
262
+
263
+ @layer nitro-kit.state {
264
+ @media (hover: hover) {
265
+ :where([data-nk="button"]:hover) {
266
+ background: var(--_nk-button-hover-background);
267
+ }
268
+ }
269
+ }
270
+ ```
271
+
272
+ Never target an unqualified `[data-slot]`. Never use `transition: all`.
273
+
274
+ ## Tokens and themes
275
+
276
+ Public `--nk-*` variables cover themeable decisions: semantic colors, paired foregrounds, typography, spacing, radii, control dimensions, shadows, borders, motion, and content widths.
277
+
278
+ Buttons and data-entry controls share control heights, radii, border geometry, focus treatment, and disabled treatment, but they do not share one surface role. Inputs, selects, textareas, and unchecked controls use the general surface tokens. The raised default Button uses the public `--nk-button-default-background`, `--nk-button-default-hover-background`, `--nk-button-default-foreground`, and `--nk-button-default-border` tokens. Button-like controls such as the native file selector may consume the same treatment; do not make ordinary data-entry surfaces depend on it.
279
+
280
+ Private `--_nk-*` variables coordinate component mechanics and are not a theme API.
281
+
282
+ The browser asset order is optional third-party base CSS (such as Lexxy) → optional Tailwind adapter → generated Nitro Kit CSS → compiled Tailwind CSS when present → unlayered application theme overrides. Without Tailwind, load Nitro Kit before application overrides. `AppearanceBootstrap` precedes every stylesheet. Never edit the generated distribution asset; keep the complete supported-token inventory and copyable recipes in [`docs/customization.md`](docs/customization.md).
283
+
284
+ Do not turn structural keywords, percentages, zero values, grid mechanics, or layout breakpoints into theme tokens. Responsive layout breakpoints are fixed component API, not customizable `--nk-*` values.
285
+
286
+ Before JavaScript connects, `:root` follows `prefers-color-scheme`. Explicit `[data-theme="light"]` and `[data-theme="dark"]` contracts override it, use the same public tokens, and set the matching `color-scheme`. `data-theme` always describes the resolved light or dark appearance; store a light, dark, or system preference separately. Render Nitro's appearance bootstrap in `head` before stylesheet links so persisted explicit choices do not flash. Its fixed, hashable script body owns one idempotent document-level runtime; defaults live in script data rather than interpolated JavaScript. The runtime validates and persists preference, resolves system appearance, listens for media and cross-tab storage changes even when no picker is present, and broadcasts changes. Any number of appearance pickers only request changes and subscribe. The bootstrap must support nonce- and hash-based host CSPs and tolerate unavailable storage.
287
+
288
+ Customization tools may read, preview, and export documented public tokens. Model customizer state as an immutable, explicitly versioned value object with closed named choices and readable URL parameters; never serialize an opaque arbitrary-token blob. Exports use stable selector and declaration ordering, cover light, dark, and system fallback, and contain only documented public variables. Structural preview choices may emit copyable component-composition examples, but never component implementations. Customization tools must not expose private `--_nk-*` values, edit the generated distribution asset, generate component copies, or turn arbitrary CSS into a Nitro contract.
289
+
290
+ The optional `nitro_kit-tailwind-v4.css` adapter is a separate asset. It may map Nitro values into Tailwind v4 theme variables, but Tailwind compilation, source detection, and utilities remain application concerns. Do not add Tailwind as a Nitro runtime dependency.
291
+
292
+ ## Baseline
293
+
294
+ Nitro CSS never relies on Tailwind Preflight. Nitro ships its own global preflight in `src/stylesheets/nitro_kit/reset.css`, entirely inside the `nitro-kit.reset` cascade layer. It normalizes the whole page — box sizing, margins and padding, borders, root typography, headings, form-control typography and appearance, placeholders, tables, replaced elements, and native-control quirks — so arbitrary content inside a Nitro component behaves the same as Nitro's own markup.
295
+
296
+ Because it is layered, unlayered application CSS always wins without needing extra specificity.
297
+
298
+ Two deliberate exceptions to Preflight parity:
299
+
300
+ - `min-width: 0` applies only to `[data-nk]` roots and owned `[data-slot]` parts.
301
+ - `list-style: none` applies only to Nitro-owned lists. Prose lists keep real markers; `Typeset` states its own list styles in `nitro-kit.base`.
302
+
303
+ ## Layout sizing
304
+
305
+ Parents own external placement and available width. Components own intrinsic geometry.
306
+
307
+ - Naturally stretchable: inputs, textareas, selects, tables, broad surfaces.
308
+ - Naturally intrinsic: buttons, badges, avatars, icons, switches.
309
+ - `Flex` decides direction, alignment, distribution, wrapping, and gap through closed responsive values.
310
+ - `Grid` decides a 1–12-column equal-track collection and gap through the same responsive grammar.
311
+ - `Container` decides available width through a closed size enumeration.
312
+
313
+ `VStack` and `HStack` are not public components. Use `Flex` with an explicit `dir:`:
314
+
315
+ ```ruby
316
+ Flex(dir: :col, gap: 4, align: :stretch) do
317
+ render ProfileForm.new
318
+ render AccountActions.new
319
+ end
320
+
321
+ Flex(
322
+ dir: "col md:row",
323
+ gap: "3 md:6",
324
+ align: "stretch md:center",
325
+ justify: "start md:between"
326
+ ) do
327
+ render WorkspaceSummary.new
328
+ render WorkspaceActions.new
329
+ end
330
+ ```
331
+
332
+ Every responsive property accepts either one scalar or its own whitespace-separated string in the grammar `BASE sm:VALUE md:VALUE lg:VALUE xl:VALUE 2xl:VALUE`. The unprefixed base is required and applies mobile-first. Prefixes are fixed minimum widths: `sm` 40rem, `md` 48rem, `lg` 64rem, `xl` 80rem, and `2xl` 96rem. Reject duplicate prefixes, unknown prefixes, missing bases, and values outside the property's closed vocabulary. Normalize output to base then breakpoint order and mirror that exact string in the owned data attribute.
333
+
334
+ `Flex.new(dir:, gap: 4, align: :start, justify: :start, wrap: :nowrap)` accepts directions `row col row-reverse col-reverse`, alignments `start center end stretch baseline`, justifications `start center end between around evenly`, wraps `nowrap wrap wrap-reverse`, and gaps `0 1 2 3 4 5 6 8 10 12 16`. Responsive strings and rendered attributes use the hyphenated tokens; idiomatic scalar symbols use `:row_reverse`, `:col_reverse`, and `:wrap_reverse` and normalize to them. `Grid.new(cols:, gap: 4)` accepts columns `1..12` and the same gaps. Do not add a generic utility parser, Tailwind runtime, arbitrary/custom breakpoints, max/range/container prefixes, or property values outside these lists.
335
+
336
+ ## Application shells
337
+
338
+ Application shells own responsive chrome, not application policy. Every shell has exactly one navigation tree and one main region, plus optional unique brand and topbar regions. Account actions compose inside the topbar. Navigation uses named header, body, section, footer, spacer, divider, and item parts. Keep product routes and current-destination policy in the caller, and use native navigation and landmark elements.
339
+
340
+ Nitro owns the shell breakpoint, fixed/sticky placement, overscroll background, independent navigation scrolling, and narrow-screen disclosure behavior. Sidebar places brand/navigation in a sticky desktop sidebar; topbar places brand/navigation/actions in a sticky desktop header; hybrid combines sidebar navigation with the header. All three reflow the one navigation tree into the narrow drawer. Do not add arbitrary breakpoint, sticky-region, or route-registry options. Reflect open state through ARIA and `data-state`; expose a skip link and labelled landmarks; trap, move, and restore focus for the narrow drawer; close on Escape, backdrop, outside activation, and Turbo navigation; and release inert state, scroll locks, and listeners on disconnect. Visible desktop navigation must not retain drawer semantics, `inert`, or `aria-hidden`. Keep shell-specific public tokens semantic and limited to canvas/sidebar colors, border, sidebar width, and topbar height.
341
+
342
+ ## Rails forms
343
+
344
+ Use Rails `form_with` from Phlex with `NitroKit::FormBuilder`:
345
+
346
+ ```ruby
347
+ form_with(model:, builder: NitroKit::FormBuilder) do |form|
348
+ form.field(:email)
349
+ form.submit
350
+ end
351
+ ```
352
+
353
+ Keep Rails naming, IDs, values, CSRF, validations, multipart behavior, and error semantics. Refactor the builder for direct Phlex; do not restore `nk_form_with` or `nk_form_for`.
354
+
355
+ Default form composition uses `Fieldset` and `FieldGroup` where semantics call for them.
356
+
357
+ ## Optional Rails integrations
358
+
359
+ Lexxy is Nitro Kit's preferred Action Text editor. `FormBuilder#field(as:
360
+ :rich_text)` must preserve Lexxy's native Action Text naming, hidden input,
361
+ attachments, prompts, validation, and editor behavior while providing the
362
+ ordinary Nitro Field label, description, error, theme, and layout contract.
363
+ Nitro does not fork Lexxy or own its JavaScript.
364
+
365
+ Active Storage components preserve native inputs and ordinary form submission without JavaScript. Keep upload limits and image sizes explicit and validated. File drops are an enhancement to a labelled, keyboard-operable native input; expose progress with native progress semantics and announce status and errors. Progressive images expose exactly one accessible image while placeholders remain decorative. Reflect asynchronous progress and errors through owned state and accessible native elements.
366
+
367
+ Do not add mandatory Dropzone.js, Ransack, or image-processing dependencies to Nitro Kit. A generic component may document an app-level or gallery adapter for an optional gem, but Nitro must not absorb that gem's query, route, or authorization policy.
368
+
369
+ ## Stimulus and Hotwire
370
+
371
+ Native HTML and CSS own behavior when they already provide the required semantics. Stimulus adds the smallest missing enhancement.
372
+
373
+ - Use `details`/`summary` for disclosure, declarative `command`/`commandfor` for dialogs, native Popover for dropdown visibility, and CSS hover/focus for tooltips.
374
+ - Do not mirror browser-owned open state into `data-state`, `aria-expanded`, or hidden attributes. Use targets and values only for state Nitro genuinely owns.
375
+ - The exception is state HTML cannot express as an attribute at all. A checkbox's `indeterminate` is a DOM property with no markup form, so `Checkbox` mounts `nk--checkable` only when `indeterminate: true`, and that controller's whole scope is applying the property and owning the matching `data-state="indeterminate"`. Ordinary checked state stays native, with no controller and no mirrored `data-state`. Do not widen a controller past the one state the browser cannot express.
376
+ - Keep native state selectors such as `[open]` and `:popover-open` authoritative in CSS.
377
+ - Clean up every external listener, timer, observer, and other resource in `disconnect`.
378
+ - Avoid duplicate initialization through Turbo morphs.
379
+ - Test keyboard behavior and Turbo Drive/Frame/Stream/morph lifecycles.
380
+
381
+ ## Interface quality
382
+
383
+ - Omit `variant:` for ordinary actions so `Button` uses its `:default` treatment. Reserve `:ghost` for deliberately low-emphasis interface chrome such as compact dismiss, pagination, toolbar, and shell-navigation controls; it is not a generic secondary-action style.
384
+ - Headings use balanced wrapping; short descriptions use pretty wrapping.
385
+ - Dynamic numeric columns use tabular numerals.
386
+ - Interactive hit areas are at least 40×40px without overlapping.
387
+ - Use concentric radii for closely nested surfaces.
388
+ - Prefer subtle layered shadows for elevated surfaces and borders for true separators/form outlines.
389
+ - Interactive transitions are interruptible and declare exact transitioned properties.
390
+ - Respect `prefers-reduced-motion`.
391
+ - Use `will-change` only after observing a compositing problem.
392
+
393
+ ## Testing checklist
394
+
395
+ Every component includes:
396
+
397
+ - Direct-Phlex rendering coverage.
398
+ - Every option and invalid value.
399
+ - Reserved attribute rejection.
400
+ - Class escape output.
401
+ - Structural and component-specific accessibility assertions.
402
+ - A gallery combination page with meaningful permutations.
403
+ - Long, missing optional, disabled, validation/error, dark, and narrow-width examples where relevant.
404
+ - Behavior tests for interactive components.
405
+
406
+ The gallery uses explicit Phlex page classes and `Gallery::Catalog`. Do not add ERB component examples or infer routes from filenames. Every `Gallery::Example` pairs Preview and Code tabs. Keep the preview in the block passed to the gallery helper so `Gallery::SourceCode` can extract, highlight, and copy its executable Ruby body; use a concrete method source for inherited flow wrappers instead of duplicating snippets.
407
+
408
+ Complete application examples combine shells, navigation, forms, tables, uploads, images, and overlays in sidebar, topbar, and hybrid layouts. Appearance coverage includes zero, one, and multiple pickers; light, dark, cross-tab updates, and live system preference changes. Theme coverage proves the contract rather than a tool: the documented token set, the set declared in `tokens.css`, and the set the bundled stylesheet serves are the same set, component CSS consumes only declared public tokens, and scoped `--nk-*` overrides on an application-owned wrapper reach Nitro descendants through inheritance.
409
+
410
+ Ship a public customization guide and align the README and Rails integration docs whenever appearance, theme tokens, or shell composition changes. Document token load order, scoped light/dark overrides, CSP setup, customizer-export installation, and copyable Phlex examples; architecture notes alone are not user documentation.
411
+
412
+ Browser verification may live outside Minitest, but it must enumerate the explicit gallery catalog, prove code-source parity and escaping, and exercise interactive behavior through Turbo lifecycles.
413
+
414
+ ## Component completion checklist
415
+
416
+ - Explicit public options.
417
+ - Enumerated options validated.
418
+ - Stable `data-nk` root.
419
+ - Component-qualified slots.
420
+ - Static CSS source and generated bundle updated.
421
+ - No internal class/style output.
422
+ - No Tailwind runtime assumption.
423
+ - Direct-Phlex tests and gallery examples.
424
+ - Rails/Hotwire behavior preserved where applicable.
425
+ - Relevant `tk` ticket updated with verification notes.
@@ -0,0 +1,38 @@
1
+ /*
2
+ * Nitro Kit Tailwind CSS v4 adapter.
3
+ * Load this before both Nitro Kit and compiled Tailwind stylesheets.
4
+ */
5
+
6
+ @layer properties, theme, base, nitro-kit, components, utilities;
7
+
8
+ @layer nitro-kit.tokens {
9
+ :where(:root, [data-theme]) {
10
+ --font-sans: var(--nk-font-sans);
11
+ --font-mono: var(--nk-font-mono);
12
+ --spacing: var(--nk-space);
13
+
14
+ --color-background: var(--nk-color-canvas);
15
+ --color-foreground: var(--nk-color-foreground);
16
+ --color-muted: var(--nk-color-muted);
17
+ --color-muted-content: var(--nk-color-muted-foreground);
18
+ --color-border: var(--nk-color-border);
19
+ --color-ring: var(--nk-color-focus);
20
+ --color-primary: var(--nk-color-primary);
21
+ --color-primary-foreground: var(--nk-color-primary-foreground);
22
+ --color-destructive: var(--nk-color-danger);
23
+ --color-destructive-content: var(--nk-color-danger-content);
24
+ --color-destructive-foreground: var(--nk-color-danger-foreground);
25
+
26
+ --radius-sm: var(--nk-radius-sm);
27
+ --radius-md: var(--nk-radius-md);
28
+ --radius-lg: var(--nk-radius-lg);
29
+ --radius-xl: var(--nk-radius-xl);
30
+
31
+ --shadow-sm: var(--nk-shadow-sm);
32
+ --shadow-md: var(--nk-shadow-md);
33
+ --shadow-lg: var(--nk-shadow-lg);
34
+
35
+ --default-transition-duration: var(--nk-duration-normal);
36
+ --default-transition-timing-function: var(--nk-ease);
37
+ }
38
+ }