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,177 +2,390 @@
2
2
 
3
3
  module NitroKit
4
4
  class Dropdown < Component
5
- ITEM_VARIANTS = %i[default destructive]
5
+ PLACEMENTS = %i[bottom_start bottom_end top_start top_end].freeze
6
+ ITEM_VARIANTS = %i[default destructive].freeze
7
+ ITEM_TYPES = %i[button submit reset].freeze
6
8
 
7
- include Phlex::Rails::Helpers::LinkTo
9
+ Trigger = ::Data.define(
10
+ :text,
11
+ :variant,
12
+ :size,
13
+ :icon,
14
+ :icon_end,
15
+ :label,
16
+ :disabled,
17
+ :html,
18
+ :aria,
19
+ :data,
20
+ :css_class,
21
+ :content
22
+ )
23
+ Entry = ::Data.define(
24
+ :kind,
25
+ :text,
26
+ :href,
27
+ :icon,
28
+ :variant,
29
+ :type,
30
+ :disabled,
31
+ :html,
32
+ :aria,
33
+ :data,
34
+ :css_class,
35
+ :content
36
+ )
8
37
 
9
- def initialize(placement: nil, **attrs)
10
- @placement = placement
38
+ def initialize(
39
+ id: nil,
40
+ placement: :bottom_start,
41
+ html: {},
42
+ aria: {},
43
+ data: {},
44
+ desperately_need_a_class: nil
45
+ )
46
+ @identifier = component_id(id)
47
+ @placement = validate_choice!(:placement, placement, PLACEMENTS)
48
+ @entries = []
11
49
 
12
50
  super(
13
- attrs,
14
- data: {
15
- controller: "nk--dropdown",
16
- nk__dropdown_placement_value: placement
17
- }
51
+ component: :dropdown,
52
+ attributes: {
53
+ id: @identifier,
54
+ data: {
55
+ controller: "nk--dropdown",
56
+ placement: placement_value
57
+ }
58
+ },
59
+ html:,
60
+ aria:,
61
+ data:,
62
+ desperately_need_a_class:
18
63
  )
19
64
  end
20
65
 
21
- attr_reader :placement
66
+ attr_reader :identifier, :placement
22
67
 
23
- def view_template
24
- div(**mattr(attrs)) do
25
- yield
68
+ def view_template(&block)
69
+ collect_entries(&block)
70
+
71
+ div(**root_attributes) do
72
+ render_trigger
73
+ render_content
26
74
  end
27
75
  end
28
76
 
29
- def trigger(text = nil, as: NitroKit::Button, **attrs, &block)
30
- builder do
31
- trigger_attrs = mattr(
32
- attrs,
33
- aria: { haspopup: "true", expanded: "false" },
34
- data: { nk__dropdown_target: "trigger", action: "click->nk--dropdown#toggle" }
35
- )
77
+ def trigger(
78
+ text = nil,
79
+ variant: :default,
80
+ size: :md,
81
+ icon: nil,
82
+ icon_end: nil,
83
+ label: nil,
84
+ disabled: false,
85
+ html: {},
86
+ aria: {},
87
+ data: {},
88
+ desperately_need_a_class: nil,
89
+ &content
90
+ )
91
+ ensure_collecting!
92
+ raise ArgumentError, "Dropdown accepts exactly one trigger" if @trigger
93
+ if text.nil? && !content && icon.nil? && icon_end.nil?
94
+ raise ArgumentError, "Dropdown trigger requires text, a block, or an icon"
95
+ end
36
96
 
37
- case as
38
- when Symbol
39
- send(as, **trigger_attrs) do
40
- text_or_block(text, &block)
41
- end
42
- else
43
- render(as.new(**trigger_attrs)) do
44
- text_or_block(text, &block)
45
- end
46
- end
97
+ @trigger = Trigger.new(
98
+ text:,
99
+ variant: validate_trigger_choice!(:variant, variant, Button::VARIANTS),
100
+ size: validate_trigger_choice!(:size, size, Button::SIZES),
101
+ icon:,
102
+ icon_end:,
103
+ label:,
104
+ disabled: validate_boolean!(:disabled, disabled),
105
+ html:,
106
+ aria:,
107
+ data:,
108
+ css_class: desperately_need_a_class,
109
+ content:
110
+ )
111
+ nil
112
+ end
113
+
114
+ def title(text = nil, html: {}, aria: {}, data: {}, desperately_need_a_class: nil, &content)
115
+ ensure_collecting!
116
+ validate_content!(:title, text, content)
117
+ add_entry(
118
+ kind: :title,
119
+ text:,
120
+ html:,
121
+ aria:,
122
+ data:,
123
+ css_class: desperately_need_a_class,
124
+ content:
125
+ )
126
+ end
127
+
128
+ def item(
129
+ text = nil,
130
+ href: nil,
131
+ icon: nil,
132
+ variant: :default,
133
+ type: :button,
134
+ disabled: false,
135
+ html: {},
136
+ aria: {},
137
+ data: {},
138
+ desperately_need_a_class: nil,
139
+ &content
140
+ )
141
+ ensure_collecting!
142
+ validate_content!(:item, text, content)
143
+ variant = validate_choice!(:variant, variant, ITEM_VARIANTS)
144
+ type = validate_choice!(:type, type.to_s.to_sym, ITEM_TYPES)
145
+ disabled = validate_boolean!(:disabled, disabled)
146
+ if !href.nil? && (!href.is_a?(String) || href.empty?)
147
+ raise ArgumentError, "Dropdown item href must be nil or a non-blank String"
148
+ end
149
+
150
+ add_entry(
151
+ kind: :item,
152
+ text:,
153
+ href:,
154
+ icon:,
155
+ variant:,
156
+ type:,
157
+ disabled:,
158
+ html:,
159
+ aria:,
160
+ data:,
161
+ css_class: desperately_need_a_class,
162
+ content:
163
+ )
164
+ end
165
+
166
+ def separator(html: {}, aria: {}, data: {}, desperately_need_a_class: nil)
167
+ ensure_collecting!
168
+ add_entry(
169
+ kind: :separator,
170
+ html:,
171
+ aria:,
172
+ data:,
173
+ css_class: desperately_need_a_class
174
+ )
175
+ end
176
+
177
+ private
178
+
179
+ def collect_entries
180
+ raise ArgumentError, "Dropdown requires a declaration block" unless block_given?
181
+
182
+ @collecting = true
183
+ yield(self)
184
+ raise ArgumentError, "Dropdown requires one trigger" unless @trigger
185
+ raise ArgumentError, "Dropdown requires at least one item" unless @entries.any? { |entry| entry.kind == :item }
186
+ ensure
187
+ @collecting = false
188
+ end
189
+
190
+ def render_trigger
191
+ component = Button.new(
192
+ @trigger.text,
193
+ variant: @trigger.variant,
194
+ size: @trigger.size,
195
+ icon: @trigger.icon,
196
+ icon_end: @trigger.icon_end,
197
+ label: @trigger.label,
198
+ disabled: @trigger.disabled,
199
+ id: trigger_id,
200
+ html: @trigger.html.merge(popovertarget: @trigger.disabled ? nil : content_id),
201
+ aria: @trigger.aria.merge(haspopup: "menu"),
202
+ data: owned_data(
203
+ @trigger.data,
204
+ nk__dropdown_target: "trigger",
205
+ action: "keydown->nk--dropdown#openFromKeyboard"
206
+ ),
207
+ desperately_need_a_class: @trigger.css_class
208
+ )
209
+ if @trigger.content
210
+ render_in_slot(component, :trigger) { render(@trigger.content) }
211
+ else
212
+ render_in_slot(component, :trigger)
47
213
  end
48
214
  end
49
215
 
50
- def content(as: :div, **attrs)
51
- builder do
52
- div(
53
- **mattr(
54
- attrs,
216
+ def render_content
217
+ div(
218
+ **slot_attributes(
219
+ :content,
220
+ attributes: {
221
+ id: content_id,
55
222
  role: "menu",
56
- aria: { hidden: "true" },
57
- class: content_class,
58
- data: { nk__dropdown_target: "content" },
59
- popover: true
60
- )
61
- ) do
62
- yield
63
- end
223
+ popover: "auto",
224
+ tabindex: -1,
225
+ aria: { labelledby: trigger_id },
226
+ data: {
227
+ placement: placement_value,
228
+ nk__dropdown_target: "content",
229
+ action: "beforetoggle->nk--dropdown#rememberFocus " \
230
+ "toggle->nk--dropdown#focusOpened keydown->nk--dropdown#navigate"
231
+ }
232
+ }
233
+ )
234
+ ) do
235
+ @entries.each { |entry| render_entry(entry) }
64
236
  end
65
237
  end
66
238
 
67
- def title(text = nil, **attrs, &block)
68
- builder do
69
- div(**mattr(attrs, class: title_class)) do
70
- text_or_block(text, &block)
71
- end
239
+ def render_entry(entry)
240
+ case entry.kind
241
+ when :title then render_title(entry)
242
+ when :item then render_item(entry)
243
+ when :separator then render_separator(entry)
72
244
  end
73
245
  end
74
246
 
75
- def item(text = nil, href: nil, variant: :default, **attrs, &block)
76
- builder do
77
- common_attrs = mattr(
78
- attrs,
79
- role: "menuitem",
80
- tabindex: "-1",
81
- class: [ item_class, item_variant_class(variant) ]
247
+ def render_title(entry)
248
+ div(
249
+ **slot_attributes(
250
+ :title,
251
+ attributes: {
252
+ role: "presentation",
253
+ aria: { hidden: true }
254
+ },
255
+ html: entry.html,
256
+ aria: entry.aria,
257
+ data: entry.data,
258
+ desperately_need_a_class: entry.css_class
82
259
  )
260
+ ) { render_content_value(entry) }
261
+ end
262
+
263
+ def render_item(entry)
264
+ native_attributes = if entry.href
265
+ {
266
+ href: entry.disabled ? nil : entry.href,
267
+ tabindex: -1,
268
+ aria: entry.aria.merge(disabled: entry.disabled ? true : nil)
269
+ }
270
+ else
271
+ {
272
+ type: entry.type,
273
+ disabled: entry.disabled,
274
+ tabindex: -1,
275
+ aria: entry.aria
276
+ }
277
+ end
278
+
279
+ attributes = slot_attributes(
280
+ :item,
281
+ attributes: native_attributes.merge(
282
+ role: "menuitem",
283
+ data: {
284
+ nk__dropdown_target: "item",
285
+ action: entry.disabled ? nil : "click->nk--dropdown#select"
286
+ }
287
+ ),
288
+ html: entry.html,
289
+ data: entry.data,
290
+ variant: entry.variant,
291
+ desperately_need_a_class: entry.css_class
292
+ )
83
293
 
84
- if href
85
- link_to(href, **common_attrs) do
86
- text_or_block(text, &block)
87
- end
88
- else
89
- div(**common_attrs) do
90
- text_or_block(text, &block)
91
- end
294
+ public_send(entry.href ? :a : :button, **attributes) do
295
+ if entry.icon
296
+ span(**slot_attributes(:item_icon)) { render(Icon.new(entry.icon, size: :sm)) }
92
297
  end
298
+ render_content_value(entry)
93
299
  end
94
300
  end
95
301
 
96
- def item_to(
97
- text_or_href,
98
- href = nil,
99
- **attrs,
100
- &block
101
- )
102
- builder do
103
- if block_given?
104
- href = text_or_href
105
- text_or_href = nil
106
- end
302
+ def render_separator(entry)
303
+ hr(
304
+ **slot_attributes(
305
+ :separator,
306
+ attributes: { role: "separator" },
307
+ html: entry.html,
308
+ aria: entry.aria,
309
+ data: entry.data,
310
+ desperately_need_a_class: entry.css_class
311
+ )
312
+ )
313
+ end
107
314
 
108
- item(text_or_href, href: href, **attrs, &block)
109
- end
315
+ def render_content_value(entry)
316
+ entry.content ? render(entry.content) : plain(entry.text.to_s)
110
317
  end
111
318
 
112
- def destructive_item(*args, **attrs, &block)
113
- builder do
114
- item(*args, **attrs, variant: :destructive, &block)
115
- end
319
+ def add_entry(**attributes)
320
+ @entries << Entry.new(**{
321
+ kind: nil,
322
+ text: nil,
323
+ href: nil,
324
+ icon: nil,
325
+ variant: nil,
326
+ type: nil,
327
+ disabled: false,
328
+ html: {},
329
+ aria: {},
330
+ data: {},
331
+ css_class: nil,
332
+ content: nil
333
+ }.merge(attributes))
334
+ nil
116
335
  end
117
336
 
118
- def destructive_item_to(text_or_block, href = nil, **attrs, &block)
119
- builder do
120
- if block_given?
121
- href = text_or_block
122
- text_or_block = nil
123
- end
337
+ def validate_content!(name, text, content)
338
+ return unless text.nil? && !content
124
339
 
125
- destructive_item(text_or_block, href: href, **attrs, &block)
126
- end
340
+ raise ArgumentError, "Dropdown #{name} requires text or a block"
127
341
  end
128
342
 
129
- def separator
130
- builder do
131
- hr(class: separator_class)
132
- end
343
+ def owned_data(data, **owned)
344
+ action_key = data.keys.find { |key| key.to_s.tr("_", "-") == "action" }
345
+ app_action = action_key && data[action_key]
346
+ data.except(action_key).merge(
347
+ owned.merge(action: [ owned[:action], app_action ].compact.join(" "))
348
+ )
133
349
  end
134
350
 
135
- private
351
+ def placement_value
352
+ placement.to_s.tr("_", "-")
353
+ end
136
354
 
137
- def content_class
138
- [
139
- "isolate w-max-content absolute top-0 left-0",
140
- "p-1 bg-background text-foreground rounded-md border shadow-sm",
141
- "w-fit max-w-sm flex-col text-left",
142
- "[&[aria-hidden=true]]:hidden flex"
143
- ]
355
+ def trigger_id
356
+ "#{identifier}-trigger"
144
357
  end
145
358
 
146
- def trigger_class
147
- ""
359
+ def content_id
360
+ "#{identifier}-content"
148
361
  end
149
362
 
150
- def title_class
151
- "px-3 pt-2 pb-1.5 text-muted-content text-sm"
363
+ def ensure_collecting!
364
+ return if @collecting
365
+
366
+ raise ArgumentError, "Dropdown declarations must be inside the render block"
152
367
  end
153
368
 
154
- def item_class
155
- [
156
- "px-3 py-1.5 rounded",
157
- "font-medium truncate",
158
- "cursor-default",
159
- "flex gap-2 items-center [&>span]:truncate"
160
- ]
369
+ def validate_trigger_choice!(name, value, choices)
370
+ return value if choices.include?(value)
371
+
372
+ raise ArgumentError,
373
+ "Unknown Dropdown trigger #{name} #{value.inspect}; expected one of: #{choices.map(&:inspect).join(", ")}"
161
374
  end
162
375
 
163
- def item_variant_class(variant)
164
- case variant
165
- when :default
166
- "[&[href]]:hover:bg-muted"
167
- when :destructive
168
- "text-destructive-content [&[href]]:hover:bg-destructive [&[href]]:hover:text-white"
169
- else
170
- raise ArgumentError, "Unknown variant: #{variant.inspect}"
171
- end
376
+ # Private copy of the AppShell identifier contract. Extract a shared
377
+ # validator into Component once its owner lands.
378
+ def component_id(value)
379
+ return generated_id if value.nil?
380
+ return value if value.is_a?(String) && value.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/)
381
+
382
+ raise ArgumentError,
383
+ "Dropdown id must be nil or a String starting with a letter or digit and containing " \
384
+ "only letters, digits, hyphens, and underscores"
172
385
  end
173
386
 
174
- def separator_class
175
- "border-t my-1 -mx-1"
387
+ def generated_id
388
+ "nk-dropdown-#{SecureRandom.hex(4)}"
176
389
  end
177
390
  end
178
391
  end