nitro_kit 0.9.0 → 2.0.0.alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/plugins/marketplace.json +20 -0
  3. data/CHANGELOG.md +172 -0
  4. data/LICENSE +28 -0
  5. data/README.md +22 -15
  6. data/STYLE_GUIDE.md +425 -0
  7. data/app/assets/stylesheets/nitro_kit-tailwind-v4.css +38 -0
  8. data/app/assets/stylesheets/nitro_kit.css +7239 -0
  9. data/app/components/nitro_kit/accordion.rb +125 -65
  10. data/app/components/nitro_kit/alert.rb +80 -43
  11. data/app/components/nitro_kit/app_navigation.rb +307 -0
  12. data/app/components/nitro_kit/app_shell.rb +227 -0
  13. data/app/components/nitro_kit/appearance_bootstrap.rb +146 -0
  14. data/app/components/nitro_kit/appearance_picker.rb +156 -0
  15. data/app/components/nitro_kit/auth_shell.rb +34 -0
  16. data/app/components/nitro_kit/avatar.rb +85 -29
  17. data/app/components/nitro_kit/avatar_stack.rb +155 -9
  18. data/app/components/nitro_kit/badge.rb +43 -81
  19. data/app/components/nitro_kit/button.rb +180 -103
  20. data/app/components/nitro_kit/button_group.rb +63 -11
  21. data/app/components/nitro_kit/button_to.rb +69 -0
  22. data/app/components/nitro_kit/card.rb +42 -45
  23. data/app/components/nitro_kit/checkbox.rb +115 -62
  24. data/app/components/nitro_kit/checkbox_group.rb +131 -22
  25. data/app/components/nitro_kit/choice.rb +55 -0
  26. data/app/components/nitro_kit/combobox.rb +292 -105
  27. data/app/components/nitro_kit/command_palette.rb +412 -0
  28. data/app/components/nitro_kit/component.rb +322 -46
  29. data/app/components/nitro_kit/container.rb +32 -0
  30. data/app/components/nitro_kit/control_group.rb +45 -0
  31. data/app/components/nitro_kit/danger_zone.rb +78 -0
  32. data/app/components/nitro_kit/data_section.rb +106 -0
  33. data/app/components/nitro_kit/details_table.rb +229 -0
  34. data/app/components/nitro_kit/dialog.rb +257 -83
  35. data/app/components/nitro_kit/dropdown.rb +335 -122
  36. data/app/components/nitro_kit/dropzone.rb +326 -0
  37. data/app/components/nitro_kit/empty_state.rb +99 -0
  38. data/app/components/nitro_kit/field.rb +461 -236
  39. data/app/components/nitro_kit/field_group.rb +15 -8
  40. data/app/components/nitro_kit/fieldset.rb +37 -39
  41. data/app/components/nitro_kit/flex.rb +49 -0
  42. data/app/components/nitro_kit/form_builder.rb +313 -72
  43. data/app/components/nitro_kit/form_section.rb +79 -0
  44. data/app/components/nitro_kit/grid.rb +34 -0
  45. data/app/components/nitro_kit/icon.rb +54 -25
  46. data/app/components/nitro_kit/input.rb +97 -10
  47. data/app/components/nitro_kit/label.rb +22 -6
  48. data/app/components/nitro_kit/layout_options.rb +7 -0
  49. data/app/components/nitro_kit/page_header.rb +76 -0
  50. data/app/components/nitro_kit/pagination.rb +355 -69
  51. data/app/components/nitro_kit/pagination_bar.rb +90 -0
  52. data/app/components/nitro_kit/progressive_image.rb +181 -0
  53. data/app/components/nitro_kit/radio_button.rb +77 -55
  54. data/app/components/nitro_kit/radio_button_group.rb +107 -36
  55. data/app/components/nitro_kit/responsive_value.rb +94 -0
  56. data/app/components/nitro_kit/rich_text_area.rb +31 -0
  57. data/app/components/nitro_kit/select.rb +129 -64
  58. data/app/components/nitro_kit/settings_layout.rb +152 -0
  59. data/app/components/nitro_kit/sheet.rb +202 -0
  60. data/app/components/nitro_kit/stat_grid.rb +75 -0
  61. data/app/components/nitro_kit/switch.rb +108 -51
  62. data/app/components/nitro_kit/table.rb +237 -45
  63. data/app/components/nitro_kit/tabs.rb +168 -63
  64. data/app/components/nitro_kit/textarea.rb +81 -10
  65. data/app/components/nitro_kit/toast.rb +211 -80
  66. data/app/components/nitro_kit/toolbar.rb +75 -0
  67. data/app/components/nitro_kit/tooltip.rb +235 -34
  68. data/app/components/nitro_kit/typeset.rb +28 -0
  69. data/app/javascript/controllers/nk/app_shell_controller.js +224 -0
  70. data/app/javascript/controllers/nk/appearance_controller.js +73 -0
  71. data/app/javascript/controllers/nk/avatar_controller.js +15 -0
  72. data/app/javascript/controllers/nk/button_controller.js +56 -0
  73. data/app/javascript/controllers/nk/checkable_controller.js +41 -0
  74. data/app/javascript/controllers/nk/combobox_controller.js +314 -82
  75. data/app/javascript/controllers/nk/command_palette_controller.js +249 -0
  76. data/app/javascript/controllers/nk/dialog_controller.js +14 -10
  77. data/app/javascript/controllers/nk/dropdown_controller.js +106 -58
  78. data/app/javascript/controllers/nk/dropzone/direct_upload.js +61 -0
  79. data/app/javascript/controllers/nk/dropzone/file_rules.js +77 -0
  80. data/app/javascript/controllers/nk/dropzone/form_submit_lock.js +30 -0
  81. data/app/javascript/controllers/nk/dropzone_controller.js +482 -0
  82. data/app/javascript/controllers/nk/overlay_position.js +37 -0
  83. data/app/javascript/controllers/nk/progressive_image_controller.js +116 -0
  84. data/app/javascript/controllers/nk/tabs_controller.js +135 -23
  85. data/app/javascript/controllers/nk/toast_controller.js +112 -45
  86. data/app/javascript/controllers/nk/tooltip_controller.js +12 -47
  87. data/config/importmap.rb +2 -0
  88. data/config/locales/en.yml +98 -0
  89. data/docs/agent_guide.md +139 -0
  90. data/docs/agent_native_spec.md +422 -0
  91. data/docs/component_contracts.md +263 -0
  92. data/docs/customization.md +595 -0
  93. data/docs/hotwire.md +94 -0
  94. data/docs/initialization_prompt.md +36 -0
  95. data/docs/migration_1_to_2.md +318 -0
  96. data/docs/new_app_strategy.md +22 -0
  97. data/docs/patterns/application_foundation.md +136 -0
  98. data/docs/patterns/crud_resource.md +144 -0
  99. data/docs/patterns/destructive_action.md +105 -0
  100. data/docs/patterns/flash_and_toast.md +57 -0
  101. data/docs/patterns/inline_edit.md +59 -0
  102. data/docs/patterns/queryable_collection.md +187 -0
  103. data/docs/patterns/resource_form.md +126 -0
  104. data/docs/rails_conventions.md +95 -0
  105. data/docs/rails_integration.md +514 -0
  106. data/lib/generators/nitro_kit/install_generator.rb +48 -0
  107. data/lib/generators/nitro_kit/upgrade_smoke_tests_generator.rb +67 -0
  108. data/lib/nitro_kit/engine.rb +20 -0
  109. data/lib/nitro_kit/installation.rb +671 -0
  110. data/lib/nitro_kit/migration_inventory.rb +345 -0
  111. data/lib/nitro_kit/upgrade_smoke_test.rb +403 -0
  112. data/lib/nitro_kit/version.rb +1 -1
  113. data/lib/nitro_kit.rb +5 -42
  114. data/lib/rails/commands/nitro_kit/nitro_kit_command.rb +42 -0
  115. data/lib/tasks/nitro_kit_tasks.rake +79 -4
  116. data/plugins/nitro-kit/.codex-plugin/plugin.json +28 -0
  117. data/plugins/nitro-kit/skills/nitro-kit-hotwire/SKILL.md +67 -0
  118. data/plugins/nitro-kit/skills/nitro-kit-hotwire/agents/openai.yaml +4 -0
  119. data/plugins/nitro-kit/skills/nitro-kit-rails/SKILL.md +60 -0
  120. data/plugins/nitro-kit/skills/nitro-kit-rails/agents/openai.yaml +4 -0
  121. data/plugins/nitro-kit/skills/nitro-kit-ui/SKILL.md +62 -0
  122. data/plugins/nitro-kit/skills/nitro-kit-ui/agents/openai.yaml +4 -0
  123. data/src/stylesheets/nitro_kit/components/accordion.css +119 -0
  124. data/src/stylesheets/nitro_kit/components/alert.css +82 -0
  125. data/src/stylesheets/nitro_kit/components/app_navigation.css +156 -0
  126. data/src/stylesheets/nitro_kit/components/app_shell.css +605 -0
  127. data/src/stylesheets/nitro_kit/components/appearance_picker.css +117 -0
  128. data/src/stylesheets/nitro_kit/components/auth_shell.css +8 -0
  129. data/src/stylesheets/nitro_kit/components/avatar.css +81 -0
  130. data/src/stylesheets/nitro_kit/components/avatar_stack.css +80 -0
  131. data/src/stylesheets/nitro_kit/components/badge.css +76 -0
  132. data/src/stylesheets/nitro_kit/components/button.css +283 -0
  133. data/src/stylesheets/nitro_kit/components/button_group.css +53 -0
  134. data/src/stylesheets/nitro_kit/components/button_to.css +6 -0
  135. data/src/stylesheets/nitro_kit/components/card.css +70 -0
  136. data/src/stylesheets/nitro_kit/components/checkbox.css +160 -0
  137. data/src/stylesheets/nitro_kit/components/checkbox_group.css +61 -0
  138. data/src/stylesheets/nitro_kit/components/combobox.css +148 -0
  139. data/src/stylesheets/nitro_kit/components/command_palette.css +255 -0
  140. data/src/stylesheets/nitro_kit/components/container.css +25 -0
  141. data/src/stylesheets/nitro_kit/components/control_group.css +149 -0
  142. data/src/stylesheets/nitro_kit/components/danger_zone.css +54 -0
  143. data/src/stylesheets/nitro_kit/components/data_section.css +40 -0
  144. data/src/stylesheets/nitro_kit/components/details_table.css +53 -0
  145. data/src/stylesheets/nitro_kit/components/dialog.css +80 -0
  146. data/src/stylesheets/nitro_kit/components/dropdown.css +152 -0
  147. data/src/stylesheets/nitro_kit/components/dropzone.css +198 -0
  148. data/src/stylesheets/nitro_kit/components/empty_state.css +53 -0
  149. data/src/stylesheets/nitro_kit/components/field.css +79 -0
  150. data/src/stylesheets/nitro_kit/components/field_group.css +6 -0
  151. data/src/stylesheets/nitro_kit/components/fieldset.css +28 -0
  152. data/src/stylesheets/nitro_kit/components/flex.css +455 -0
  153. data/src/stylesheets/nitro_kit/components/form_section.css +71 -0
  154. data/src/stylesheets/nitro_kit/components/grid.css +308 -0
  155. data/src/stylesheets/nitro_kit/components/icon.css +26 -0
  156. data/src/stylesheets/nitro_kit/components/input.css +80 -0
  157. data/src/stylesheets/nitro_kit/components/label.css +9 -0
  158. data/src/stylesheets/nitro_kit/components/layout.css +440 -0
  159. data/src/stylesheets/nitro_kit/components/page_header.css +61 -0
  160. data/src/stylesheets/nitro_kit/components/pagination.css +119 -0
  161. data/src/stylesheets/nitro_kit/components/pagination_bar.css +41 -0
  162. data/src/stylesheets/nitro_kit/components/palette.css +113 -0
  163. data/src/stylesheets/nitro_kit/components/progressive_image.css +139 -0
  164. data/src/stylesheets/nitro_kit/components/radio_button.css +127 -0
  165. data/src/stylesheets/nitro_kit/components/radio_button_group.css +143 -0
  166. data/src/stylesheets/nitro_kit/components/rich_text_area.css +21 -0
  167. data/src/stylesheets/nitro_kit/components/select.css +70 -0
  168. data/src/stylesheets/nitro_kit/components/settings_layout.css +100 -0
  169. data/src/stylesheets/nitro_kit/components/sheet.css +100 -0
  170. data/src/stylesheets/nitro_kit/components/stat_grid.css +51 -0
  171. data/src/stylesheets/nitro_kit/components/switch.css +152 -0
  172. data/src/stylesheets/nitro_kit/components/table.css +116 -0
  173. data/src/stylesheets/nitro_kit/components/tabs.css +138 -0
  174. data/src/stylesheets/nitro_kit/components/textarea.css +39 -0
  175. data/src/stylesheets/nitro_kit/components/toast.css +131 -0
  176. data/src/stylesheets/nitro_kit/components/toolbar.css +78 -0
  177. data/src/stylesheets/nitro_kit/components/tooltip.css +124 -0
  178. data/src/stylesheets/nitro_kit/components/typeset.css +191 -0
  179. data/src/stylesheets/nitro_kit/layers.css +7 -0
  180. data/src/stylesheets/nitro_kit/reset.css +192 -0
  181. data/src/stylesheets/nitro_kit/tokens.css +198 -0
  182. metadata +182 -51
  183. data/Rakefile +0 -8
  184. data/app/assets/tailwind/application.css +0 -49
  185. data/app/components/nitro_kit/datepicker.rb +0 -9
  186. data/app/helpers/nitro_kit/accordion_helper.rb +0 -9
  187. data/app/helpers/nitro_kit/alert_helper.rb +0 -11
  188. data/app/helpers/nitro_kit/avatar_helper.rb +0 -13
  189. data/app/helpers/nitro_kit/badge_helper.rb +0 -13
  190. data/app/helpers/nitro_kit/button_group_helper.rb +0 -9
  191. data/app/helpers/nitro_kit/button_helper.rb +0 -49
  192. data/app/helpers/nitro_kit/card_helper.rb +0 -9
  193. data/app/helpers/nitro_kit/checkbox_helper.rb +0 -50
  194. data/app/helpers/nitro_kit/combobox_helper.rb +0 -9
  195. data/app/helpers/nitro_kit/datepicker_helper.rb +0 -9
  196. data/app/helpers/nitro_kit/dialog_helper.rb +0 -9
  197. data/app/helpers/nitro_kit/dropdown_helper.rb +0 -9
  198. data/app/helpers/nitro_kit/field_group_helper.rb +0 -9
  199. data/app/helpers/nitro_kit/field_helper.rb +0 -9
  200. data/app/helpers/nitro_kit/fieldset_helper.rb +0 -9
  201. data/app/helpers/nitro_kit/form_helper.rb +0 -13
  202. data/app/helpers/nitro_kit/icon_helper.rb +0 -9
  203. data/app/helpers/nitro_kit/input_helper.rb +0 -37
  204. data/app/helpers/nitro_kit/label_helper.rb +0 -19
  205. data/app/helpers/nitro_kit/pagination_helper.rb +0 -46
  206. data/app/helpers/nitro_kit/radio_button_helper.rb +0 -23
  207. data/app/helpers/nitro_kit/select_helper.rb +0 -24
  208. data/app/helpers/nitro_kit/switch_helper.rb +0 -9
  209. data/app/helpers/nitro_kit/table_helper.rb +0 -9
  210. data/app/helpers/nitro_kit/tabs_helper.rb +0 -9
  211. data/app/helpers/nitro_kit/textarea_helper.rb +0 -9
  212. data/app/helpers/nitro_kit/toast_helper.rb +0 -25
  213. data/app/helpers/nitro_kit/tooltip_helper.rb +0 -9
  214. data/app/javascript/controllers/nk/accordion_controller.js +0 -18
  215. data/app/javascript/controllers/nk/datepicker_controller.js +0 -5
  216. data/app/javascript/controllers/nk/switch_controller.js +0 -32
  217. data/lib/generators/nitro_kit/component_generator.rb +0 -84
  218. data/lib/nitro_kit/schema_builder.rb +0 -129
  219. data/lib/nitro_kit/variants.rb +0 -21
@@ -2,103 +2,208 @@
2
2
 
3
3
  module NitroKit
4
4
  class Tabs < Component
5
- def initialize(default: nil, **attrs)
6
- @default = default
7
- @id = attrs[:id] || SecureRandom.hex(6)
5
+ ORIENTATIONS = %i[horizontal vertical].freeze
6
+ ACTIVATIONS = %i[automatic manual].freeze
7
+ Tab = ::Data.define(:key, :label, :disabled, :content)
8
+
9
+ def initialize(
10
+ default: nil,
11
+ id:,
12
+ label: I18n.t("nitro_kit.tabs.label"),
13
+ orientation: :horizontal,
14
+ activation: :automatic,
15
+ html: {},
16
+ aria: {},
17
+ data: {},
18
+ desperately_need_a_class: nil
19
+ )
20
+ @identifier = component_id(id)
21
+ @default = default.nil? ? nil : normalize_identity(default, name: "default tab key")
22
+ @label = validate_label!(label)
23
+ @orientation = validate_choice!(:orientation, orientation, ORIENTATIONS)
24
+ @activation = validate_choice!(:activation, activation, ACTIVATIONS)
25
+ @tabs = []
8
26
 
9
27
  super(
10
- attrs,
11
- data: { controller: "nk--tabs", nk__tabs_active_value: default },
12
- class: base_class
28
+ component: :tabs,
29
+ attributes: {
30
+ id: @identifier,
31
+ data: {
32
+ controller: "nk--tabs",
33
+ orientation: @orientation,
34
+ activation: @activation,
35
+ nk__tabs_active_value: @default || "",
36
+ nk__tabs_orientation_value: @orientation,
37
+ nk__tabs_activation_value: @activation
38
+ }
39
+ },
40
+ html:,
41
+ aria:,
42
+ data:,
43
+ desperately_need_a_class:
13
44
  )
14
45
  end
15
46
 
16
- attr_reader :default, :id
47
+ attr_reader :identifier, :default, :label, :orientation, :activation
48
+
49
+ def view_template(&block)
50
+ collect_tabs(&block)
51
+ active_key = resolve_active_key
17
52
 
18
- def view_template
19
- div(**attrs) do
20
- yield
53
+ div(**root_attributes_for(active_key)) do
54
+ render_tablist(active_key)
55
+ @tabs.each { |tab| render_panel(tab, active_key:) }
21
56
  end
22
57
  end
23
58
 
24
- def tabs(**attrs)
25
- builder do
26
- div(**mattr, role: "tabtabs", class: tabs_class) do
27
- yield
28
- end
59
+ def tab(key, label, disabled: false, &content)
60
+ ensure_collecting!
61
+
62
+ key = normalize_identity(key, name: "tab key")
63
+ label = validate_label!(label)
64
+ disabled = validate_boolean!(:disabled, disabled)
65
+ raise ArgumentError, "Tab #{key.inspect} requires panel content" unless content
66
+ raise ArgumentError, "Duplicate tab key #{key.inspect}" if find_tab(key)
67
+
68
+ @tabs << Tab.new(key:, label:, disabled:, content:)
69
+ nil
70
+ end
71
+
72
+ private
73
+
74
+ def collect_tabs
75
+ raise ArgumentError, "Tabs requires a tab declaration block" unless block_given?
76
+
77
+ @collecting = true
78
+ yield(self)
79
+ raise ArgumentError, "Tabs requires at least one tab" if @tabs.empty?
80
+ ensure
81
+ @collecting = false
82
+ end
83
+
84
+ def render_tablist(active_key)
85
+ div(
86
+ **slot_attributes(
87
+ :list,
88
+ attributes: {
89
+ role: "tablist",
90
+ aria: {
91
+ label:,
92
+ orientation:
93
+ }
94
+ }
95
+ )
96
+ ) do
97
+ @tabs.each { |tab| render_tab(tab, active_key:) }
29
98
  end
30
99
  end
31
100
 
32
- def tab(key, text = nil, **attrs, &block)
33
- builder do
34
- button(
35
- **mattr(
36
- attrs,
101
+ def render_tab(tab, active_key:)
102
+ active = tab.key == active_key
103
+
104
+ button(
105
+ **slot_attributes(
106
+ :tab,
107
+ attributes: {
108
+ id: tab_id(tab),
109
+ type: "button",
110
+ role: "tab",
111
+ disabled: tab.disabled,
112
+ tabindex: tab.disabled ? -1 : 0,
37
113
  aria: {
38
- selected: (default == key).to_s,
39
- controls: tab_id(key, :panel)
114
+ controls: panel_id(tab),
115
+ selected: active
40
116
  },
41
- class: tab_class,
42
117
  data: {
43
- action: "nk--tabs#setActiveTab keydown.left->nk--tabs#prevTab keydown.right->nk--tabs#nextTab",
44
- key:,
45
- nk__tabs_key_param: key,
118
+ key: tab.key,
119
+ state: active ? "active" : "inactive",
120
+ action: "click->nk--tabs#select keydown->nk--tabs#navigate",
46
121
  nk__tabs_target: "tab"
47
- },
48
- id: tab_id(key, :tab),
49
- role: "tab",
50
- tabindex: default == key ? 0 : -1
51
- )
52
- ) do
53
- text_or_block(text, &block)
54
- end
55
- end
122
+ }
123
+ }
124
+ )
125
+ ) { tab.label }
56
126
  end
57
127
 
58
- def panel(key, **attrs)
59
- builder do
60
- div(
61
- **mattr(
62
- attrs,
128
+ def render_panel(tab, active_key:)
129
+ active = tab.key == active_key
130
+
131
+ div(
132
+ **slot_attributes(
133
+ :panel,
134
+ attributes: {
135
+ id: panel_id(tab),
136
+ role: "tabpanel",
63
137
  aria: {
64
- hidden: (default != key).to_s,
65
- labelledby: tab_id(key, :tab)
138
+ labelledby: tab_id(tab)
66
139
  },
67
- class: panel_class,
68
140
  data: {
69
- key:,
141
+ key: tab.key,
142
+ state: active ? "active" : "inactive",
70
143
  nk__tabs_target: "panel"
71
- },
72
- id: tab_id(key, :panel),
73
- name: key,
74
- role: "tabpanel"
75
- )
76
- ) do
77
- yield
78
- end
144
+ }
145
+ }
146
+ )
147
+ ) do
148
+ render(tab.content)
79
149
  end
80
150
  end
81
151
 
82
- private
152
+ def resolve_active_key
153
+ enabled_tabs = @tabs.reject(&:disabled)
154
+ raise ArgumentError, "Tabs requires at least one enabled tab" if enabled_tabs.empty?
155
+
156
+ return enabled_tabs.first.key unless default
157
+
158
+ selected = find_tab(default)
159
+ raise ArgumentError, "Default tab key #{default.inspect} is not declared" unless selected
160
+ raise ArgumentError, "Default tab #{default.inspect} cannot be disabled" if selected.disabled
161
+
162
+ selected.key
163
+ end
164
+
165
+ def root_attributes_for(active_key)
166
+ attributes = root_attributes
167
+
168
+ attributes.merge(
169
+ data: attributes.fetch(:data).merge(nk__tabs_active_value: active_key)
170
+ )
171
+ end
83
172
 
84
- def tab_id(key, suffix)
85
- "#{id}-#{key}-#{suffix}"
173
+ def tab_id(tab)
174
+ "#{identifier}-#{tab.key}-tab"
86
175
  end
87
176
 
88
- def base_class
89
- "w-full"
177
+ def panel_id(tab)
178
+ "#{identifier}-#{tab.key}-panel"
90
179
  end
91
180
 
92
- def tabs_class
93
- "flex gap-4 border-b h-10"
181
+ def find_tab(key)
182
+ @tabs.find { |tab| tab.key == key }
94
183
  end
95
184
 
96
- def tab_class
97
- "border-b-2 border-transparent hover:border-primary focus-visible:border-primary cursor-pointer text-muted-content aria-[selected=true]:text-foreground font-medium aria-[selected=true]:border-primary -mb-px px-2"
185
+ def ensure_collecting!
186
+ return if @collecting
187
+
188
+ raise ArgumentError, "Tabs must be declared inside the render block"
189
+ end
190
+
191
+ def validate_label!(value)
192
+ return value if value.is_a?(String) && value.present?
193
+
194
+ raise ArgumentError, "Tabs labels must be non-blank Strings"
98
195
  end
99
196
 
100
- def panel_class
101
- "aria-[hidden=true]:hidden py-4"
197
+ def validate_boolean!(name, value)
198
+ return value if value == true || value == false
199
+
200
+ raise ArgumentError, "Tabs #{name} must be true or false"
201
+ end
202
+
203
+ def component_id(value)
204
+ return value if value.is_a?(String) && value.present? && !value.match?(/\s/)
205
+
206
+ raise ArgumentError, "Tabs id must be a non-blank String without whitespace"
102
207
  end
103
208
  end
104
209
  end
@@ -2,29 +2,100 @@
2
2
 
3
3
  module NitroKit
4
4
  class Textarea < Component
5
- def initialize(value: nil, **attrs)
5
+ WRAPS = %i[soft hard off].freeze
6
+ OWNED_ATTRIBUTES = %i[
7
+ id name value placeholder disabled readonly required autocomplete rows cols minlength
8
+ maxlength wrap
9
+ ].freeze
10
+
11
+ def initialize(
12
+ id: nil,
13
+ name: nil,
14
+ value: nil,
15
+ placeholder: nil,
16
+ disabled: false,
17
+ readonly: false,
18
+ required: false,
19
+ autocomplete: nil,
20
+ rows: nil,
21
+ cols: nil,
22
+ minlength: nil,
23
+ maxlength: nil,
24
+ wrap: nil,
25
+ html: {},
26
+ aria: {},
27
+ data: {},
28
+ desperately_need_a_class: nil
29
+ )
6
30
  @value = value
31
+ disabled = validate_boolean!(:disabled, disabled)
32
+ readonly = validate_boolean!(:readonly, readonly)
33
+ required = validate_boolean!(:required, required)
34
+ rows = validate_positive_integer!(:rows, rows)
35
+ cols = validate_positive_integer!(:cols, cols)
36
+ minlength = validate_non_negative_integer!(:minlength, minlength)
37
+ maxlength = validate_non_negative_integer!(:maxlength, maxlength)
38
+ if minlength && maxlength && minlength > maxlength
39
+ raise ArgumentError, "minlength cannot exceed maxlength"
40
+ end
41
+ wrap = validate_choice!(:wrap, wrap.to_s.to_sym, WRAPS) unless wrap.nil?
42
+ reject_owned_attributes!(html)
7
43
 
8
44
  super(
9
- attrs,
10
- class: default_class
45
+ component: :textarea,
46
+ attributes: {
47
+ id:,
48
+ name:,
49
+ placeholder:,
50
+ disabled:,
51
+ readonly:,
52
+ required:,
53
+ autocomplete:,
54
+ rows:,
55
+ cols:,
56
+ minlength:,
57
+ maxlength:,
58
+ wrap:
59
+ }.compact,
60
+ html:,
61
+ aria:,
62
+ data:,
63
+ desperately_need_a_class:
11
64
  )
12
65
  end
13
66
 
14
67
  attr_reader :value
15
68
 
16
69
  def view_template
17
- textarea(**attrs) { plain(value) }
70
+ textarea(**root_attributes) { plain(value.to_s) }
18
71
  end
19
72
 
20
73
  private
21
74
 
22
- def default_class
23
- [
24
- "rounded-md border bg-background border-border text-base px-3 py-2",
25
- # Focus
26
- "focus:outline-none ring-ring ring-offset-2 ring-offset-background focus-visible:ring-2"
27
- ]
75
+ def reject_owned_attributes!(html)
76
+ return unless html.is_a?(Hash)
77
+
78
+ html.each_key do |key|
79
+ normalized = key.to_s.downcase.tr("_", "-")
80
+ owned = OWNED_ATTRIBUTES.find { |name| name.to_s.tr("_", "-") == normalized }
81
+ next unless owned
82
+
83
+ raise ArgumentError, "#{normalized} is owned by Textarea; pass #{owned}: as a keyword"
84
+ end
85
+ end
86
+
87
+ def validate_positive_integer!(name, value)
88
+ return if value.nil?
89
+ return value if value.is_a?(Integer) && value.positive?
90
+
91
+ raise ArgumentError, "#{name} must be a positive Integer"
92
+ end
93
+
94
+ def validate_non_negative_integer!(name, value)
95
+ return if value.nil?
96
+ return value if value.is_a?(Integer) && value >= 0
97
+
98
+ raise ArgumentError, "#{name} must be a non-negative Integer"
28
99
  end
29
100
  end
30
101
  end